Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3d013f1
docs(task): design organization Nowledge vertical
xiaoland Sep 8, 2026
a8c929d
feat(organization): add semantic organization behaviors
xiaoland Sep 9, 2026
2def36c
docs(organization): 记录 preview 两轮验收与未通过结论
xiaoland Sep 10, 2026
cebf2fa
feat(agent): 增加可关闭的开发追踪并启用 PR100 调试
xiaoland Sep 10, 2026
2bc7b97
fix(organization): 改善 Agent 工具合同与纠错反馈
xiaoland Sep 10, 2026
4b69dd9
fix(organization): 收口查询约束并记录端到端基线
xiaoland Sep 10, 2026
6ac43f0
fix(organization): 保留草稿错误层级并补全细化定义
xiaoland Sep 10, 2026
faa74ba
fix(organization): 批量读取实体并修正行为结束指导
xiaoland Sep 11, 2026
008d535
docs(organization): 记录批量入口重验结果与待复核修复
xiaoland Sep 11, 2026
9a7ab93
fix(organization): 使用普通数组表达实体读取参数
xiaoland Sep 11, 2026
fb291dc
docs(organization): 记录普通数组修复的真实验收证据
xiaoland Sep 11, 2026
f4362ad
fix(organization): 逐项保留实体类型并停止成功后的确认复读
xiaoland Sep 11, 2026
9581fa5
docs(organization): 记录逐项引用与无需确认指导的复测
xiaoland Sep 11, 2026
2dac3e1
fix(organization): 明确检索契约并简化行为引导
xiaoland Sep 11, 2026
a64d6f6
docs(organization): 记录最小引导复测结果与语义残余
xiaoland Sep 11, 2026
5fef0fd
fix(organization): 恢复反刍专用工具与提示词
xiaoland Sep 12, 2026
17ec054
docs(organization): 记录专用反刍组合复验结果
xiaoland Sep 12, 2026
ebf220a
fix(organization): 解除结束探索对穷尽信息的隐含要求
xiaoland Sep 12, 2026
80708a8
docs(organization): 记录探索结束指导的复测与残余
xiaoland Sep 12, 2026
90177ca
fix(organization): 区分来源忠实性与证据支持
xiaoland Sep 12, 2026
bf16ebd
fix(organization): 明确来源忠实性排除项的判断对象
xiaoland Sep 12, 2026
d855790
docs(organization): 记录 evidence stance 单行为复测结果
xiaoland Sep 12, 2026
7bb868c
fix(organization): 先辨认目标命题再判断证据贡献
xiaoland Sep 12, 2026
a3eaff2
docs(organization): 记录命题识别 SOP 的部分改善与残余
xiaoland Sep 12, 2026
a31af18
docs(organization): 复审合并条件并清理临时调试设施
xiaoland Sep 12, 2026
3d04cfd
fix(organization): 局部候选失败后继续自动整理
xiaoland Sep 12, 2026
4a0f266
fix(organization): 用迭代环检测恢复长链读取
xiaoland Sep 12, 2026
db48582
docs(organization): 记录读取复验与维护性审查结论
xiaoland Sep 12, 2026
48ed482
fix(organization): 在线程内完成 lineage 同步读取
xiaoland Sep 12, 2026
d7ed6fc
docs(organization): 记录小图复验与合并准备结论
xiaoland Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/100.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add extensible semantic Organization behaviors, bounded graph-use queries, and Human-reviewed
acceptance fixtures, composable Agent read tools and opt-in development traces; also recognize schema-v3 SVC development
database provider configuration.
2 changes: 2 additions & 0 deletions app/business/agent/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ transport policy.
- Tool registration is decorator-owned. Exact persisted Tool IDs have set semantics and are bound once per new Thread;
later registry changes do not rewrite existing Thread schemas or handlers.
- Cancellation owns no rollback, retry, shielding, or compensation. Completed Tool effects remain.
- `OBSRV__AGENT_DEBUG` enables development events through existing logging. These are diagnostic records, not execution
persistence or recovery authority. Preserve the actual ToolResult and Turn outcome when changing debug instrumentation.
51 changes: 51 additions & 0 deletions app/business/agent/debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Opt-in development traces through the existing logging backend."""

import asyncio
import datetime
import json
import sys
import typing
import uuid

import pydantic

from app.settings import settings
from libs.obsrv.log_record import ENABLE_LOG_BACKEND, TRACE_ID
from libs.obsrv.main import get_logger


def _json_value(value: typing.Any) -> typing.Any:
if isinstance(value, pydantic.BaseModel):
return value.model_dump(mode="python")
if isinstance(value, bytes):
return {"omitted_binary_bytes": len(value)}
if isinstance(value, datetime.datetime | datetime.date):
return value.isoformat()
if isinstance(value, uuid.UUID):
return str(value)
raise TypeError(f"Unsupported Agent trace value: {type(value).__name__}")


def _emit(event: str, thread_id: str, payload: dict[str, typing.Any]) -> None:
token = ENABLE_LOG_BACKEND.set(True)
try:
envelope = {
"event": event,
"thread_id": thread_id,
"trace_id": TRACE_ID.get(),
**payload,
}
get_logger().getChild("agent.debug").info(
json.dumps(envelope, ensure_ascii=False, default=_json_value),
extra={"event": event, "agent_thread_id": thread_id},
)
except Exception as error:
# A debug destination must never replace the Agent's real outcome.
sys.stderr.write(f"Agent debug trace unavailable: {type(error).__name__}\n")
finally:
ENABLE_LOG_BACKEND.reset(token)


async def trace(event: str, thread_id: uuid.UUID, **payload: typing.Any) -> None:
if settings.obsrv.agent_debug:
await asyncio.to_thread(_emit, event, str(thread_id), payload)
8 changes: 8 additions & 0 deletions app/business/agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
ThreadState,
)
from .thread import Thread
from .debug import trace


HandlerT = typing.TypeVar("HandlerT", bound=typing.Callable[..., typing.Any])
Expand Down Expand Up @@ -158,6 +159,13 @@ async def run(cls, agent_id: AgentID, initial_message: UserMessage) -> Thread:
messages=(SystemMessage(content=definition.system_prompt),),
)
thread_id, persisted = await cls._persistence.create(state)
await trace(
"agent.thread.created",
thread_id,
agent_id=agent_id,
agent_name=definition.name,
state=persisted,
)
thread = Thread(thread_id, persisted, cls._persistence, bound_tools)
thread.start_turn(initial_message)
return thread
Expand Down
122 changes: 116 additions & 6 deletions app/business/agent/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from enum import StrEnum
import inspect
import json
import logging
import time
import traceback
import typing

import pydantic
Expand All @@ -23,9 +24,11 @@

from .contracts import AgentTurnActiveError, BoundAgentTool, ToolExecutionError
from .persistence import ThreadID, ThreadPersistenceBackend, ThreadState
from .debug import trace
from libs.obsrv.main import get_logger


logger = logging.getLogger(__name__)
logger = get_logger().getChild("agent.thread")


class TurnTermination(StrEnum):
Expand All @@ -48,6 +51,8 @@ def __init__(
self._persistence = persistence
self._tools = {tool.definition.id: tool for tool in tools}
self.current_turn: asyncio.Task[TurnTermination] | None = None
self._turn_index = 0
self._model_calls = 0

@property
def messages(self):
Expand Down Expand Up @@ -85,18 +90,79 @@ def start_turn(self, input: UserMessage) -> asyncio.Task[TurnTermination]:
return self.current_turn

async def _run_turn(self, input: UserMessage) -> TurnTermination:
self._turn_index += 1
self._model_calls = 0
started = time.monotonic()
await trace(
"agent.turn.started",
self.id,
turn=self._turn_index,
input=input,
model=self.model,
max_model_calls=self.max_model_calls_per_turn,
)
try:
outcome = await self._execute_turn(input)
except asyncio.CancelledError:
await trace(
"agent.turn.finished",
self.id,
turn=self._turn_index,
model_calls=self._model_calls,
outcome="cancelled",
elapsed_seconds=time.monotonic() - started,
)
raise
except Exception as error:
await trace(
"agent.turn.finished",
self.id,
turn=self._turn_index,
model_calls=self._model_calls,
outcome="failed",
error_type=type(error).__name__,
error=str(error),
traceback=traceback.format_exc(),
elapsed_seconds=time.monotonic() - started,
)
raise
await trace(
"agent.turn.finished",
self.id,
turn=self._turn_index,
model_calls=self._model_calls,
outcome=outcome,
elapsed_seconds=time.monotonic() - started,
)
return outcome

async def _execute_turn(self, input: UserMessage) -> TurnTermination:
self._state = await self._persistence.discard_trailing_incomplete_tool_calls(self.id)
self._state = await self._persistence.append(self.id, (input,))
model_calls = 0

while True:
model_calls += 1
self._model_calls += 1
await trace(
"agent.model.started",
self.id,
turn=self._turn_index,
call=self._model_calls,
)
started = time.monotonic()
assistant = await AIManager.chat(
self._state.model,
self._state.messages,
self._state.tools,
self._state.tool_choice,
)
await trace(
"agent.model.completed",
self.id,
turn=self._turn_index,
call=self._model_calls,
response=assistant,
elapsed_seconds=time.monotonic() - started,
)
if not assistant.tool_calls:
self._state = await self._persistence.append(self.id, (assistant,))
return TurnTermination.COMPLETED
Expand All @@ -106,7 +172,7 @@ async def _run_turn(self, input: UserMessage) -> TurnTermination:
self.id,
(assistant, ToolResultMessage(results=results)),
)
if model_calls >= self._state.max_model_calls_per_turn:
if self._model_calls >= self._state.max_model_calls_per_turn:
return TurnTermination.MAX_MODEL_CALLS

async def _execute_tool_batch(
Expand All @@ -123,6 +189,39 @@ async def _execute_tool_batch(
return tuple(await asyncio.gather(*tasks))

async def _execute_tool_call(self, call: ToolCall) -> ToolResult:
await trace(
"agent.tool.started",
self.id,
turn=self._turn_index,
call=self._model_calls,
tool_call=call,
)
started = time.monotonic()
try:
result = await self._invoke_tool_call(call)
except asyncio.CancelledError:
await trace(
"agent.tool.cancelled",
self.id,
turn=self._turn_index,
call=self._model_calls,
tool_call_id=call.id,
tool=call.tool,
elapsed_seconds=time.monotonic() - started,
)
raise
await trace(
"agent.tool.completed",
self.id,
turn=self._turn_index,
call=self._model_calls,
tool=call.tool,
result=result,
elapsed_seconds=time.monotonic() - started,
)
return result

async def _invoke_tool_call(self, call: ToolCall) -> ToolResult:
tool = self._tools.get(call.tool)
if tool is None:
return ToolResult(
Expand Down Expand Up @@ -151,7 +250,18 @@ async def _execute_tool_call(self, call: ToolCall) -> ToolResult:
content=error.content,
is_error=True,
)
except Exception:
except Exception as error:
await trace(
"agent.tool.exception",
self.id,
turn=self._turn_index,
call=self._model_calls,
tool_call_id=call.id,
tool=call.tool,
error_type=type(error).__name__,
error=str(error),
traceback=traceback.format_exc(),
)
logger.exception("Unexpected Agent Tool failure", extra={"tool": call.tool})
return ToolResult(
tool_call_id=call.id,
Expand Down
Loading
Loading