feat(agent): align the document tools with the harness-trained shape — ls, read(path), grep(pattern, path?)
This commit is contained in:
+31
-33
@@ -49,27 +49,28 @@ outline (0 when absent) and the per-turn log line records
|
||||
|
||||
Agent document tools (phase 37, PLAN §4 extension, owner permission
|
||||
2026-08-26; phase 45 removed the per-tool budgets — owner permission
|
||||
2026-08-27; phase 68 added the ``search_documents`` grep): a
|
||||
2026-08-27; phase 70 aligned the surface to the harness-trained
|
||||
``ls`` / ``read`` / ``grep`` — owner permission 2026-09-03): a
|
||||
**grounded** turn (``not plan.deflected``) no longer streams a bare
|
||||
``chat_stream`` — it runs the agent loop (``app.rag.agent.run_agent``),
|
||||
which offers the model the three server-side tools
|
||||
``list_documents`` / ``read_document`` / ``search_documents`` for the
|
||||
whole turn (as many calls as the model wants, re-lists and re-searches
|
||||
included) until it answers or the round cap (``BOR_AGENT_MAX_ROUNDS``,
|
||||
default 10) forces one final no-tools answer. Each model-requested call
|
||||
streams as an SSE ``tool`` event — ``{"type": "tool", "name": …,
|
||||
"argument": "source/path" | pattern | null}`` — ahead of the answer's
|
||||
``delta`` frames: ``argument`` is the read document's path for
|
||||
``read_document``, the raw search pattern for ``search_documents``
|
||||
(a non-string pattern — a model error the backend refuses — yields
|
||||
null), and null for ``list_documents``. ``done.sources``,
|
||||
``query_log.sources`` and the per-turn log line all report the same
|
||||
combined source list (retrieval docs + the agent's read docs, deduped
|
||||
by ``(source, path)``, order preserved — a search adds no source; it is
|
||||
a locator, locked A5), and the log line records ``tool_calls=N`` after
|
||||
``thinking_chars=N`` (PLAN §9 line extension — ``N`` counts executed
|
||||
tool calls; rejected calls do not count). **Deflected turns keep the
|
||||
direct ``chat_stream`` — byte-identical to the pre-phase path (A8):**
|
||||
``ls`` / ``read`` / ``grep`` for the whole turn (as many calls as the
|
||||
model wants, re-lists and re-greps included) until it answers or the
|
||||
round cap (``BOR_AGENT_MAX_ROUNDS``, default 10) forces one final
|
||||
no-tools answer. Each model-requested call streams as an SSE ``tool``
|
||||
event — ``{"type": "tool", "name": …, "argument": … | null}`` — ahead
|
||||
of the answer's ``delta`` frames: ``argument`` is the single string the
|
||||
model passed — ``read``'s ``path`` (the combined ``source/path``),
|
||||
``grep``'s ``pattern``, ``ls``'s ``path`` — or null (a non-string
|
||||
value — a model error the backend refuses — and an omitted argument
|
||||
both yield null). ``done.sources``, ``query_log.sources`` and the
|
||||
per-turn log line all report the same combined source list (retrieval
|
||||
docs + the agent's read docs, deduped by ``(source, path)``, order
|
||||
preserved — a grep adds no source; it is a locator, locked A5), and the
|
||||
log line records ``tool_calls=N`` after ``thinking_chars=N`` (PLAN §9
|
||||
line extension — ``N`` counts executed tool calls; rejected calls do not
|
||||
count). **Deflected turns keep the direct ``chat_stream`` —
|
||||
byte-identical to the pre-phase path (A8):**
|
||||
the LOW prompt never carries tools, and with ``agent_max_rounds`` at
|
||||
**0** ``run_agent`` makes exactly one ``tools=None`` request,
|
||||
reproducing the pre-phase behavior (the kill switch).
|
||||
@@ -400,21 +401,18 @@ async def chat(
|
||||
try:
|
||||
async for piece in answer_stream: # StreamPiece | ToolCallPiece | RetryPiece
|
||||
if isinstance(piece, ToolCallPiece):
|
||||
# Phase 37 (PLAN §4 extension): one SSE ``tool``
|
||||
# frame per model-requested call. ``argument`` is
|
||||
# the read_document "source/path"; phase 68
|
||||
# extends it with the search_documents pattern
|
||||
# (a non-string pattern — a model error the
|
||||
# backend refuses — is null); null otherwise.
|
||||
if piece.name == "read_document":
|
||||
argument = (
|
||||
f"{piece.arguments.get('source')}/{piece.arguments.get('path')}"
|
||||
)
|
||||
elif piece.name == "search_documents":
|
||||
pattern = piece.arguments.get("pattern")
|
||||
argument = pattern if isinstance(pattern, str) else None
|
||||
else:
|
||||
argument = None
|
||||
# Phase 37 (PLAN §4 extension; phase 70): one SSE
|
||||
# ``tool`` frame per model-requested call.
|
||||
# ``argument`` is the single string the model
|
||||
# passed — ``read``'s ``path`` (the combined
|
||||
# ``source/path``), ``grep``'s ``pattern``,
|
||||
# ``ls``'s ``path`` — or null (a non-string value
|
||||
# is a model error the backend refuses, as is an
|
||||
# omitted argument).
|
||||
argument = piece.arguments.get(
|
||||
"pattern" if piece.name == "grep" else "path"
|
||||
)
|
||||
argument = argument if isinstance(argument, str) else None
|
||||
yield sse_event(
|
||||
ChatToolEvent(name=piece.name, argument=argument).model_dump()
|
||||
)
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ def doc_format(path: str) -> str:
|
||||
|
||||
|
||||
@router.get("/docs", response_model=DocList)
|
||||
def list_documents(
|
||||
def list_indexed_documents(
|
||||
db: Session = Depends(get_db), # noqa: B008
|
||||
_admin: None = Depends(require_admin), # noqa: B008
|
||||
) -> DocList:
|
||||
|
||||
Reference in New Issue
Block a user