Merge remote-tracking branch 'origin/master' into worktree/provider-routed-llm-adapters

# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/sandbox-acp-agent/cordis.yml
#	examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl
#	packages/compact/compact-basic/README.md
#	packages/compact/compact-basic/src/index.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent-loop/tests/properties.spec.ts
#	packages/core/session/README.md
#	packages/core/session/src/types.ts
#	packages/core/session/tests/derived-cache.spec.ts
#	packages/llm/llm-deepseek/src/index.ts
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm-pi-ai/src/convert.ts
#	packages/llm/llm-pi-ai/tests/adapter.spec.ts
#	packages/llm/llm/README.md
#	packages/llm/llm/src/call-config.ts
#	packages/llm/llm/src/index.ts
#	packages/ui/acp-agent/src/index.ts
#	packages/ui/acp/tests/harness.ts
#	packages/ui/jsonrpc/README.md
#	packages/ui/jsonrpc/src/server.ts
#	packages/ui/stdio-agent/README.md
#	packages/ui/stdio-agent/src/index.ts
#	python/sdk/README.i18n.yaml
This commit is contained in:
Yichen Jiang
2026-07-14 22:17:50 +08:00
672 changed files with 10295 additions and 14200 deletions

View File

@@ -23,3 +23,23 @@ The tool writes only the session event; it does not render. UIs subscribe to `se
## Export shape
A function/namespace plugin: it exports `name` / `inject` / `apply` and NO default. A stray `export default` would collapse the module via the Loader's `unwrapExports` and drop `inject` (see [docs/postmortem/0001](../../../docs/postmortem/0001-acp-default-export-drops-inject.md)).
## Model Experience
### Tool schema
**What the model sees**: The model sees the generated [`todo_write` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-todo).
**Token effect**: Fixed schema cost on every request where the tool is visible.
### Tool-call history and result
**What the model sees**: Each assistant tool call retains the entire replacement list in its arguments. Success returns exactly `Updated todo list: <pending> pending, <inProgress> in progress, <completed> completed.` Stable failures are ``Error: invalid todo: `content` must be a non-empty string``, `Error: invalid todos: duplicate content "<content>"`, `Error: invalid todos: at most one task may be in_progress, got <count>`, and `Error: todo_write requires an owning agent session`. The full `todo/write` session event is UI and replay state, not a second model message.
**Token effect**: Token growth scales with every full list the model submits, and those call arguments remain until compaction. The result itself is small and fixed-shape.
## Known Limitations and Deferred Work
- **Single-owner scope only** — the list belongs to the one calling agent session; subagent/shared/swarm scopes are a deliberate cut (see § Single owner), and a non-agent caller is rejected.
- **The item shape is deliberately minimal** — `content` plus three-state `status`; no id, priority, or active-form fields, and the ACP bridge synthesizes the `priority` ACP requires.
- **Whole-list replacement is the only operation** — no partial updates, no read-back tool; the model must resend the entire list each call.

View File

@@ -1,21 +1,7 @@
/**
* The model-facing `todo_write` tool: the agent's whole task list, replaced
* wholesale on each call. Every call appends a `todo/write` event (the full
* list snapshot) to the calling agent's session log via
* `exec.agent.session.append('todo/write', { todos })`; the current list is the
* most recent such event (last-write-wins on replay). UIs render off
* `session/event`: the stdio UI prints the checklist, the ACP bridge maps it to
* a `plan` sessionUpdate.
*
* Single owner: the list belongs to the ONE agent session that called the tool.
* There is no subagent/shared/swarm scope — a non-agent caller (no
* `exec.agent`) has nowhere to write the list and is rejected.
*
* Plugin export shape: named exports, NO default. The cordis Loader's
* `unwrapExports` does `exports.default ?? exports`, so a stray default would
* collapse the module to the bare `apply` and drop `inject`, crashing at load
* (see docs/postmortem/0001).
*
* Model-facing whole-list replacement. Each call appends a `todo/write` snapshot to the calling
* agent's session; replay is last-write-wins, and UIs render from session events. A non-agent
* caller has no owning list and is rejected. Named exports preserve loader injection metadata.
* @module @deepseek-ai/dsh-tool-todo
*/
@@ -42,20 +28,9 @@ const DESCRIPTION =
+ '(not started), `in_progress` (being worked on now), `completed` (finished).'
/**
* Validate the value constraints the SchemaSpec can't express and build the
* canonical {@link TodoItem}[].
*
* `defineTool` already validates type/required/enum before `execute` runs (a
* bad `status` is rejected by the registry's `validateArgs`, never reaching
* here), so `status` is guaranteed to be one of the three enum literals. But
* `InferArgs` maps an `enum` string prop to plain `string`, so the compiler sees
* `args.todos` as `{ content: string; status: string }[]`; the
* `status as TodoItem['status']` narrowing records that registry guarantee
* rather than re-checking it (an unreachable re-check would be dead code the
* coverage gate would flag). What remains is the
* value rules the DSL has no vocabulary for: non-empty unique content (stored
* trimmed, so the persisted value matches the dedupe/length key), and at most
* one `in_progress` task.
* Validate the value constraints the SchemaSpec can't express and build the canonical {@link
* TodoItem}[]: trimmed non-empty unique content and at most one in-progress item. The registry
* has already enforced the status enum; the cast below records that guarantee.
*/
function toTodoList(raw: { content: string; status: string }[]): TodoItem[] {
const todos: TodoItem[] = []

View File

@@ -149,10 +149,7 @@ describe('dsh-tool-todo', () => {
})
it('has the namespace-plugin export shape (no stray default) so the Loader keeps name/inject/apply', () => {
// Postmortem 0001 guard: this plugin HAS `inject = ['tools']`, so a stray
// `export default apply` would collapse the module via `unwrapExports`
// (`exports.default ?? exports`), DROP `inject`, and crash at load with
// "cannot get property … without inject". Guard the shape directly.
// A default export would make Loader unwrap only apply and drop `inject`.
expect('default' in tool).toBe(false)
expect(tool.name).toBe('tool-todo')
expect(tool.inject).toEqual(['tools'])