From c23214be5602a4d66cb1d70baa4ddaa4dae965c8 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:30:45 +0800 Subject: [PATCH] refactor(core): fold initiator scope into agents --- AGENTS.md | 2 +- docs/architecture.md | 7 +- docs/capability-seams.md | 9 +- docs/config-catalog.md | 7 +- docs/cordis-catalog/events.md | 356 ++++++- docs/cordis-catalog/services.md | 902 +++++++++++++++++- docs/core-data-structures/core.md | 44 +- docs/module-graph.md | 10 +- docs/rfc/INDEX.md | 2 +- .../2026-07-15-agent-execution-context.md | 61 -- .../2026-07-15-agent-execution-context.zh.md | 61 -- ...026-07-15-agent-initiator-scope.i18n.yaml} | 4 +- .../2026-07-15-agent-initiator-scope.md | 63 ++ .../2026-07-15-agent-initiator-scope.zh.md | 63 ++ examples/coding-agent/tests/code-mode.e2e.ts | 3 - .../context/workspace-context/package.json | 1 - .../tests/workspace-context.e2e.ts | 2 - .../tests/workspace-context.spec.ts | 2 - .../cordis/tool-cordis/src/api-catalog.ts | 19 +- packages/core/README.md | 7 +- packages/core/agent-execution/README.md | 23 - packages/core/agent-execution/package.json | 31 - packages/core/agent-execution/src/index.ts | 140 --- packages/core/agent-execution/src/types.ts | 12 - .../tests/agent-execution.spec.ts | 163 ---- packages/core/agent-execution/tsconfig.json | 21 - packages/core/agent-loop/README.md | 4 +- packages/core/agent-loop/package.json | 2 - packages/core/agent-loop/src/agent.ts | 2 +- packages/core/agent-loop/src/index.ts | 3 +- ...cution.spec.ts => agent-initiator.spec.ts} | 101 +- packages/core/agent-loop/tests/agent.spec.ts | 6 +- packages/core/agent-loop/tests/cancel.spec.ts | 4 - .../tests/config-session-id.spec.ts | 8 - .../tests/contract-regressions.spec.ts | 9 - .../agent-loop/tests/coverage-edges.spec.ts | 2 - .../agent-loop/tests/interception.spec.ts | 2 - packages/core/agent-loop/tests/loop.spec.ts | 4 - .../core/agent-loop/tests/properties.spec.ts | 2 - .../agent-loop/tests/request-cache.e2e.ts | 2 - .../tests/request-reconstruction.spec.ts | 2 - packages/core/agent-loop/tests/resume.spec.ts | 9 - .../agent-loop/tests/scope-lifecycle.spec.ts | 2 - .../core/agent-loop/tests/tool-calls.spec.ts | 3 - .../core/agent-loop/tests/tool-order.spec.ts | 2 - .../core/agent-loop/tests/turn-stop.spec.ts | 2 - packages/core/agent-loop/tsconfig.json | 3 - packages/core/agent/README.md | 17 +- packages/core/agent/package.json | 2 +- packages/core/agent/src/index.ts | 121 ++- .../core/agent/tests/agent-initiator.spec.ts | 163 ++++ .../agent/tests/gen-cordis-catalog.spec.ts | 27 +- packages/examples/agent-spine-demo/README.md | 3 +- .../examples/agent-spine-demo/package.json | 4 +- .../examples/agent-spine-demo/src/index.ts | 2 - .../examples/agent-spine-demo/tsconfig.json | 3 - .../sdk/helper/src/features/builtin/spine.ts | 4 - packages/support/agent-loop-testkit/README.md | 2 +- .../support/agent-loop-testkit/package.json | 2 - .../support/agent-loop-testkit/src/index.ts | 2 - .../support/agent-loop-testkit/tsconfig.json | 3 - .../workflow-workerthread/package.json | 1 - .../tests/workflow-workerthread.e2e.ts | 2 - pnpm-lock.yaml | 27 - python/sdk-runtime/package.json | 1 - scripts/cordis-walk.ts | 41 +- scripts/gen-cordis-api.ts | 4 +- scripts/gen-cordis-catalog.ts | 83 +- scripts/gen-doc-graphs.ts | 12 +- scripts/gen-website-api.ts | 27 +- scripts/type-equiv.manifest.json | 2 - .../verify-package-readme-model-experience.ts | 1 - tsconfig.build.json | 1 - tsconfig.json | 1 - website/.vitepress/config/api-sidebar.json | 4 - website/zh-CN/api/harness/agent-execution.md | 48 - website/zh-CN/api/harness/agent-loop.md | 8 +- website/zh-CN/api/harness/agents.md | 77 +- website/zh-CN/api/harness/events.md | 2 +- 79 files changed, 1883 insertions(+), 1003 deletions(-) delete mode 100644 docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.md delete mode 100644 docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.zh.md rename docs/rfc/implemented/architecture/{2026-07-15-agent-execution-context.i18n.yaml => 2026-07-15-agent-initiator-scope.i18n.yaml} (64%) create mode 100644 docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md create mode 100644 docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md delete mode 100644 packages/core/agent-execution/README.md delete mode 100644 packages/core/agent-execution/package.json delete mode 100644 packages/core/agent-execution/src/index.ts delete mode 100644 packages/core/agent-execution/src/types.ts delete mode 100644 packages/core/agent-execution/tests/agent-execution.spec.ts delete mode 100644 packages/core/agent-execution/tsconfig.json rename packages/core/agent-loop/tests/{agent-execution.spec.ts => agent-initiator.spec.ts} (76%) create mode 100644 packages/core/agent/tests/agent-initiator.spec.ts delete mode 100644 website/zh-CN/api/harness/agent-execution.md diff --git a/AGENTS.md b/AGENTS.md index a0ef9c7539..38799b8e96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ DeepSeek Harness SDK is a plugin-based agent harness on vendored Cordis: **every ``` vendor/ Vendored Cordis source — manifest + sync procedure in vendor/README.md packages/ @deepseek-ai/dsh- workspaces at packages/// - core/ product API spine: session, system-prompt, tools, agent, agent-execution, agent-loop + core/ product API spine: session, system-prompt, tools, agent, agent-loop prompt/ workspace instructions llm/ LLM seam + the DeepSeek adapters (hand-rolled + pi-ai design twin) bash/ bash executor seam + local impl + model-facing bash tools diff --git a/docs/architecture.md b/docs/architecture.md index 09cc43afeb..f791784e8e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,8 +16,7 @@ A harness is one [Cordis](cordis-primer.md) context. Packages contribute service | `ctx.sessions` | `dsh-session` | in-memory event-sourced sessions | | `ctx.systemPrompt` | `dsh-system-prompt` | ordered prompt sections, tool schemas, and prompt variables | | `ctx.tools` | `dsh-tools` | tool registry and [execution pipeline](tool-execution-pipeline.md) | -| `ctx.agents` | `dsh-agent` | live agent registry, public `Agent` handle, `agent/*` events | -| `ctx.agentExecution` | `dsh-agent-execution` | process-local ambient Agent identity for asynchronous driver work | +| `ctx.agents` | `dsh-agent` | live agents, creation delegation, `agent/*` events, and process-local initiating Agent scope | | `ctx.agentLoop` | `dsh-agent-loop` | concrete `Agent` driver | ### Capability Services @@ -119,9 +118,9 @@ Every session event is turn-enclosed. Reloading preserves an interrupted tail an Every live agent owns a scoped `agent.ctx`. Its registrations shadow globals, receive only that agent's dispatches, and unwind with it; async effects such as background-task cleanup are awaited. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. Typed resolvers derive carrier checks from merged `Events` signatures and `scopeTarget` ([semantic-gates RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md)). See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md) and [subagent composition controls](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md). -### Agent Execution Context +### Initiating Agent Scope -`AgentLoop` establishes process-local `ctx.agentExecution` around each driver; the [decision](rfc/implemented/architecture/2026-07-15-agent-execution-context.md) owns boundary and explicit-identity rules. +`AgentLoop` runs each process-local driver inside `ctx.agents.withInitiator()`; the [decision](rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns boundary and explicit-identity rules. ## State diff --git a/docs/capability-seams.md b/docs/capability-seams.md index feb31a358e..93c5335e61 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -48,9 +48,7 @@ flowchart LR pkg_skill["skill"] svc_skills["ctx.skills
Skill provider registry"] pkg_skill_local["skill-local"] - svc_agents["ctx.agents
Agent registry"] - pkg_agent_execution["agent-execution"] - svc_agentExecution["ctx.agentExecution
Agent execution context"] + svc_agents["ctx.agents
Agent service"] svc_agentLoop["ctx.agentLoop
Concrete loop driver"] pkg_agent_spine_demo["agent-spine-demo"] pkg_bash["bash"] @@ -99,7 +97,6 @@ flowchart LR pkg_acp --> svc_approval pkg_acp --> svc_userInteraction pkg_agent --> svc_agents - pkg_agent_execution --> svc_agentExecution pkg_agent_loop --> svc_agentLoop pkg_approval --> svc_approval pkg_bash --> svc_bash @@ -145,7 +142,6 @@ flowchart LR pkg_web_search_perplexity --> svc_web pkg_workflow --> svc_workflows pkg_workflow_workerthread --> svc_workflows - svc_agentExecution --> pkg_agent_loop svc_agentLoop --> pkg_agent_spine_demo svc_agents --> pkg_acp svc_agents --> pkg_agent_loop @@ -216,8 +212,7 @@ flowchart LR | `ctx.tools` | `core` | [`tools`](../packages/core/tools) | - | [`agent-loop`](../packages/core/agent-loop), [`tool-ask-user`](../packages/ui/tool-ask-user), [`tool-bash`](../packages/bash/tool-bash), [`tool-cordis`](../packages/cordis/tool-cordis), [`tool-fs`](../packages/fs/tool-fs), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent), [`tool-todo`](../packages/todo/tool-todo), [`tool-web`](../packages/web/tool-web), [`acp`](../packages/ui/acp) | - | Registers capabilities, owns Code Mode transport, and routes calls through pre-policy, monotonic guards, around dispatch, post-policy, and final-result observation. | | `ctx.userInteraction` | `seam` | [`user-interaction`](../packages/ui/user-interaction) | [`stdio-demo`](../packages/examples/stdio-demo), [`acp`](../packages/ui/acp) | [`tool-ask-user`](../packages/ui/tool-ask-user), [`stdio-demo`](../packages/examples/stdio-demo), [`acp`](../packages/ui/acp) | - | UI front doors provide the active human-answer provider; tool-ask-user pauses a tool call on the provider-neutral ask() promise. | | `ctx.skills` | `seam` | [`skill`](../packages/skill/skill) | [`skill-local`](../packages/skill/skill-local) | [`tool-skill`](../packages/skill/tool-skill) | - | Merges provider skill catalogs; tool-skill renders the session-prefix catalog and loads complete skill bodies. | -| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-demo`](../packages/examples/stdio-demo), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles and the create/resume factory seam. | -| `ctx.agentExecution` | `core` | [`agent-execution`](../packages/core/agent-execution) | - | [`agent-loop`](../packages/core/agent-loop) | - | Carries the exact initiating Agent across one process-local asynchronous driver chain; explicit identities remain authoritative at external boundaries. | +| `ctx.agents` | `core` | [`agent`](../packages/core/agent) | - | [`agent-loop`](../packages/core/agent-loop), [`acp`](../packages/ui/acp), [`subagent-inprocess`](../packages/subagent/subagent-inprocess), [`stdio-demo`](../packages/examples/stdio-demo), [`invariants`](../packages/support/invariants) | - | Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation. | | `ctx.agentLoop` | `bundle` | [`agent-loop`](../packages/core/agent-loop) | - | [`agent-spine-demo`](../packages/examples/agent-spine-demo) | - | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. | | `ctx.bash` | `seam` | [`bash`](../packages/bash/bash) | [`bash-local`](../packages/bash/bash-local), [`bash-sandbox`](../packages/bash/bash-sandbox) | [`tool-bash`](../packages/bash/tool-bash), [`hooks-claude`](../packages/hooks/hooks-claude), [`hooks-codex`](../packages/hooks/hooks-codex) | - | The model-facing bash tools and hook bridges consume this seam; sandboxed or remote executors replace bash-local without touching them. | | `ctx.bashEnv` | `core` | [`tool-bash`](../packages/bash/tool-bash) | - | - | - | Plugins declare effect-scoped DSH_* facts; tool-bash collects one trusted snapshot per execution and the executor rebuilds the namespace. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index ee6003c6f6..47c7ff90b7 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -75,7 +75,7 @@ Source: [`packages/examples/acp-demo/src/index.ts:33`](../packages/examples/acp- ## `@deepseek-ai/dsh-agent-loop` -Requires: `agents` · `agentExecution` · `sessions` · `llm` · `tools` · `systemPrompt` +Requires: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` ```ts config-catalog /** Agent-loop plugin configuration. */ @@ -101,7 +101,7 @@ export interface Config { Depends on: [`AgentOptions`](../packages/core/agent/src/index.ts) · [`SessionId`](../packages/core/session/src/index.ts) -Source: [`packages/core/agent-loop/src/index.ts:370`](../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:369`](../packages/core/agent-loop/src/index.ts) ## `@deepseek-ai/dsh-agent-spine-demo` @@ -157,7 +157,7 @@ export interface SkillConfig { Depends on: [`AgentLoopConfig`](#deepseek-aidsh-agent-loop) · [`SkillLocal`](../packages/skill/skill-local/src/index.ts) · [`SkillRegistryConfig`](#deepseek-aidsh-skill) · [`SystemPromptConfig`](#deepseek-aidsh-system-prompt) · [`toolBash`](../packages/bash/tool-bash/src/index.ts) · [`ToolsConfig`](#deepseek-aidsh-tools) · [`toolSkill`](../packages/skill/tool-skill/src/index.ts) · [`toolTasks`](../packages/tasks/tool-tasks/src/index.ts) · [`workspaceContext`](../packages/context/workspace-context/src/index.ts) -Source: [`packages/examples/agent-spine-demo/src/index.ts:58`](../packages/examples/agent-spine-demo/src/index.ts) +Source: [`packages/examples/agent-spine-demo/src/index.ts:57`](../packages/examples/agent-spine-demo/src/index.ts) ## `@deepseek-ai/dsh-bash-local` @@ -1415,7 +1415,6 @@ Source: [`packages/context/workspace-context/src/config.ts:16`](../packages/cont These load from a `cordis.yml` entry with no `config:` block; they declare no config surface. - `@deepseek-ai/dsh-agent` ([`packages/core/agent/src/index.ts`](../packages/core/agent/src/index.ts)) -- `@deepseek-ai/dsh-agent-execution` ([`packages/core/agent-execution/src/index.ts`](../packages/core/agent-execution/src/index.ts)) - `@deepseek-ai/dsh-fs-policy` ([`packages/fs/fs-policy/src/index.ts`](../packages/fs/fs-policy/src/index.ts)) - `@deepseek-ai/dsh-invariants` — requires `sessions` ([`packages/support/invariants/src/index.ts`](../packages/support/invariants/src/index.ts)) - `@deepseek-ai/dsh-llm` ([`packages/llm/llm/src/index.ts`](../packages/llm/llm/src/index.ts)) diff --git a/docs/cordis-catalog/events.md b/docs/cordis-catalog/events.md index 4598390194..12f877ad48 100644 --- a/docs/cordis-catalog/events.md +++ b/docs/cordis-catalog/events.md @@ -3,9 +3,9 @@ # Cordis Events Catalog -Every cordis event a plugin can listen to: exact signature, dispatch mode, and the declaration's JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. +Every cordis event a plugin can listen to: exact signature, dispatch mode, and original declaration JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. -This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence (skipped by doc-typecheck, since a bare signature is not standalone-compilable). Type names in a signature link to the page that documents them. +This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them. The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns, grouped by scope. The **inherited tier** at the end is the cordis-core + loader/hmr/timer event surface a plugin also sees — pinned vendor source, summarized tersely. @@ -18,6 +18,16 @@ Dispatch modes: **emit** (fire-and-forget), **waterfall** (each listener gets `n A fully configured agent and live session were published. Setup is composition-only; `agent/session-start` is the first startup-driving seam. Synchronous listener failure vetoes publication, while returned-promise rejection is reported. Detach requested during dispatch waits until every creation listener has observed the stable entry. ```ts cordis-catalog +/** + * A fully configured agent and live session were published. Setup is + * composition-only; `agent/session-start` is the first startup-driving seam. + * Synchronous listener failure vetoes publication, while returned-promise + * rejection is reported. Detach requested during dispatch waits until every + * creation listener has observed the stable entry. + * @param agent - the newly registered agent with its live session and completed setup. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode emit + */ 'agent/created'(this: Scoped, agent: Agent): void ``` @@ -30,6 +40,14 @@ Source: [`packages/core/agent/src/types.ts:141`](../../packages/core/agent/src/t An agent left the registry; AgentLoop emits this after driver quiescence but before session detachment and scoped-registration unwind. Custom registry users own their driver-ordering contract. ```ts cordis-catalog +/** + * An agent left the registry; AgentLoop emits this after driver quiescence + * but before session detachment and scoped-registration unwind. Custom + * registry users own their driver-ordering contract. + * @param agent - the exact agent removed from the registry. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode emit + */ 'agent/disposed'(this: Scoped, agent: Agent): void ``` @@ -42,6 +60,16 @@ Source: [`packages/core/agent/src/types.ts:150`](../../packages/core/agent/src/t A step or turn errored. The loop reports a failure here (plus the logger) even when the error has no in-turn position for a session `error` event. ```ts cordis-catalog +/** + * A step or turn errored. The loop reports a failure here (plus the logger) + * even when the error has no in-turn position for a session `error` event. + * @param agent - the agent whose turn errored. + * @param turn - the turn in which the failure surfaced. + * @param step - the step at which the failure surfaced. + * @param error - the failure, verbatim. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode emit + */ 'agent/error'(this: Scoped, agent: Agent, turn: number, step: number, error: Error): void ``` @@ -54,6 +82,22 @@ Source: [`packages/core/agent/src/types.ts:285`](../../packages/core/agent/src/t Awaited serial checkpoint for session-surface mutation after prompt assembly and before `step/start`; appends land outside the pending step. The loop derives history once afterward, so compaction records and replacements are included without rewriting an assembled request. The prompt and prefix are the exact pressure inputs for that request, and `signal` cancels listener work. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. ```ts cordis-catalog +/** + * Awaited serial checkpoint for session-surface mutation after prompt + * assembly and before `step/start`; appends land outside the pending step. + * The loop derives history once afterward, so compaction records and + * replacements are included without rewriting an assembled request. The + * prompt and prefix are the exact pressure inputs for that request, and + * `signal` cancels listener work. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @param agent - the agent opening the step. + * @param turn - the open turn number. + * @param step - the pending step number. + * @param fullSystemPrompt - the assembled prompt. + * @param sessionPrefix - the frozen request prefix. + * @param signal - the turn abort signal. + * @mode serial + */ 'agent/pre-step'(this: Scoped, agent: Agent, turn: number, step: number, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal): Promise | void ``` @@ -66,6 +110,15 @@ Source: [`packages/core/agent/src/types.ts:204`](../../packages/core/agent/src/t Allow, rewrite, or block one drained prompt before it becomes a user message. Call `next()` for the unchanged default. ```ts cordis-catalog +/** + * Allow, rewrite, or block one drained prompt before it becomes a user + * message. Call `next()` for the unchanged default. + * @param agent - the agent draining its inbox. + * @param content - the drained message's blocks, as queued. + * @param source - the message's resolved source. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode waterfall + */ 'agent/prompt-submit'(this: Scoped, agent: Agent, content: ContentBlock[], source: MessageSource, next: () => Promise): Promise ``` @@ -78,6 +131,15 @@ Source: [`packages/core/agent/src/types.ts:214`](../../packages/core/agent/src/t Detached, frozen content entered the agent's inbox. Source defaults have already been applied, so these are the exact values retained for the log. ```ts cordis-catalog +/** + * Detached, frozen content entered the agent's inbox. Source defaults have + * already been applied, so these are the exact values retained for the log. + * @param agent - the agent whose inbox received the message. + * @param content - the accepted content blocks retained by the inbox. + * @param info - the accepted source plus whether it entered as steering. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode emit + */ 'agent/queued'(this: Scoped, agent: Agent, content: ContentBlock[], info: { source: MessageSource; steering: boolean }): void ``` @@ -90,6 +152,17 @@ Source: [`packages/core/agent/src/types.ts:169`](../../packages/core/agent/src/t Replace the frozen call configuration. Model-visible content must use logged channels; this seam cannot mutate messages. Injection here joins the next request because the current step boundary is already fixed. ```ts cordis-catalog +/** + * Replace the frozen call configuration. Model-visible content must use + * logged channels; this seam cannot mutate messages. Injection here joins + * the next request because the current step boundary is already fixed. + * @param agent - the agent making the model call. + * @param turn - the open turn number. + * @param step - the step whose request this is. + * @param config - the config the loop would use (frozen); return a replacement to switch. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode waterfall + */ 'agent/request'(this: Scoped, agent: Agent, turn: number, step: number, config: LlmCallConfig, next: () => Promise): Promise ``` @@ -102,6 +175,20 @@ Source: [`packages/core/agent/src/types.ts:226`](../../packages/core/agent/src/t Compose request-only messages placed before derived history. The frozen result is computed once per loop instance, logged on its anchoring request header, and reused so the provider prefix remains stable. Interrupted composition is discarded. Composition precedes the first `agent/pre-step` and request boundary, so listener appends join the current request and pressure accounting sees the composed prefix. Changing context belongs in history; contributors should prepend to `await next()` to preserve registration order. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. ```ts cordis-catalog +/** + * Compose request-only messages placed before derived history. The frozen + * result is computed once per loop instance, logged on its anchoring request + * header, and reused so the provider prefix remains stable. Interrupted + * composition is discarded. Composition precedes the first `agent/pre-step` + * and request boundary, so listener appends join the current request and + * pressure accounting sees the composed prefix. Changing context belongs in + * history; contributors should prepend to `await next()` to preserve registration order. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @param agent - the agent whose session prefix is being composed. + * @param prefix - the frozen seed; return an extended replacement. + * @param signal - aborts composition when the step is torn down. + * @mode waterfall + */ 'agent/session-prefix'(this: Scoped, agent: Agent, prefix: Message[], signal: AbortSignal, next: () => Promise): Promise ``` @@ -114,6 +201,16 @@ Source: [`packages/core/agent/src/types.ts:241`](../../packages/core/agent/src/t The session lifecycle began, once before the first turn. Use `agent.inject()` to seed model-facing context. This is a notification, not a veto; disposal requested by a lifecycle owner is rechecked before the driver starts. ```ts cordis-catalog +/** + * The session lifecycle began, once before the first turn. Use + * `agent.inject()` to seed model-facing context. This is a notification, not + * a veto; disposal requested by a lifecycle owner is rechecked before the + * driver starts. + * @param agent - the agent whose session lifecycle began. + * @param source - why the session started (fresh startup, resume, …). + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode emit + */ 'agent/session-start'(this: Scoped, agent: Agent, source: SessionStartSource): void ``` @@ -126,6 +223,14 @@ Source: [`packages/core/agent/src/types.ts:182`](../../packages/core/agent/src/t Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does not enter `running` synchronously; drive lifecycle from this event. ```ts cordis-catalog +/** + * Agent status changed (`idle` ⇄ `running`, or → `disposed`). `send()` does + * not enter `running` synchronously; drive lifecycle from this event. + * @param agent - the agent whose status flipped. + * @param status - the status just entered (the transition's destination). + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode emit + */ 'agent/status'(this: Scoped, agent: Agent, status: AgentStatus): void ``` @@ -138,6 +243,16 @@ Source: [`packages/core/agent/src/types.ts:159`](../../packages/core/agent/src/t Waterfall: post-process the assembled assistant Message before tool dispatch (validation, content rewriting, …). ```ts cordis-catalog +/** + * Waterfall: post-process the assembled assistant {@link Message} before + * tool dispatch (validation, content rewriting, …). + * @param agent - the agent that received the step's response. + * @param turn - the open turn number. + * @param step - the step that produced the message. + * @param message - the assistant message as assembled from the stream. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode waterfall + */ 'agent/step-result'(this: Scoped, agent: Agent, turn: number, step: number, message: Message, next: () => Promise): Promise ``` @@ -150,6 +265,15 @@ Source: [`packages/core/agent/src/types.ts:252`](../../packages/core/agent/src/t Override whether the turn continues. The default continues after tool calls or steering and stops otherwise; a continue reason becomes steering. ```ts cordis-catalog +/** + * Override whether the turn continues. The default continues after tool + * calls or steering and stops otherwise; a continue reason becomes steering. + * @param agent - the agent deciding whether to run another step. + * @param turn - the turn being continued or stopped. + * @param defaultDecision - what the loop would do absent an override. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode waterfall + */ 'agent/turn-continuation'(this: Scoped, agent: Agent, turn: number, defaultDecision: ContinuationDecision, next: () => Promise): Promise ``` @@ -162,6 +286,15 @@ Source: [`packages/core/agent/src/types.ts:262`](../../packages/core/agent/src/t Monotonic terminal-stop checkpoint after continuation and steering are folded; a stop remains authoritative through turn close and flush: steering queued in that window is discarded, while ordinary sends survive. ```ts cordis-catalog +/** + * Monotonic terminal-stop checkpoint after continuation and steering are + * folded; a stop remains authoritative through turn close and flush: + * steering queued in that window is discarded, while ordinary sends survive. + * @param agent - the agent whose composed continuation outcome may be stopped. + * @param turn - the turn at its terminal-stop checkpoint. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @mode serial + */ 'agent/turn-stop'(this: Scoped, agent: Agent, turn: number): ContinuationStop | undefined ``` @@ -176,10 +309,19 @@ Source: [`packages/core/agent/src/types.ts:272`](../../packages/core/agent/src/t A declarative agent entry failed before it could publish a live agent. Consumers that buffer work for the configured identity use this transient signal to reject that work instead of waiting forever. Normal factory teardown suppresses failures from the cancelled startup attempt. ```ts cordis-catalog +/** + * A declarative agent entry failed before it could publish a live agent. + * Consumers that buffer work for the configured identity use this + * transient signal to reject that work instead of waiting forever. Normal + * factory teardown suppresses failures from the cancelled startup attempt. + * @param sessionId - exact shared agent/session identity that failed startup. + * @param error - persistence, setup, or publication failure. + * @mode emit + */ 'agent-loop/config-start-failed'(sessionId: SessionId, error: unknown): void ``` -Source: [`packages/core/agent-loop/src/index.ts:363`](../../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:362`](../../packages/core/agent-loop/src/index.ts) ## `approval/*` @@ -188,6 +330,13 @@ Source: [`packages/core/agent-loop/src/index.ts:363`](../../packages/core/agent- Ask composed answerers for one decision. Return an outcome to claim the request or call `next()`; failure yields the fail-closed default. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. ```ts cordis-catalog +/** + * Ask composed answerers for one decision. Return an outcome to claim the + * request or call `next()`; failure yields the fail-closed default. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent. + * @param req - the pending decision (agent, tool identity, reason, signal). + * @mode waterfall + */ 'approval/request'(this: Scoped, req: ApprovalRequest, next: () => Promise): Promise ``` @@ -202,6 +351,13 @@ Source: [`packages/ui/user-approval/src/index.ts:31`](../../packages/ui/user-app Single-slot decision for the next FileSystem.editText. Calling `next()` yields an unconditional edit; the first returned guard wins. ```ts cordis-catalog +/** + * Single-slot decision for the next {@link FileSystem.editText}. Calling + * `next()` yields an unconditional edit; the first returned guard wins. + * @param target - the resolved target about to be edited. + * @param actor - the opaque tool-execution context the decider keys off. + * @mode waterfall + */ 'fs/edit-intent'(target: FsTarget, actor: object | undefined, next: () => { version: FsVersion } | undefined | Promise<{ version: FsVersion } | undefined>): Promise<{ version: FsVersion } | undefined> ``` @@ -214,6 +370,14 @@ Source: [`packages/fs/fs/src/index.ts:61`](../../packages/fs/fs/src/index.ts) Record a successful observation. Listeners must be synchronous recorders: throws fail the tool call and returned promises are not awaited. ```ts cordis-catalog +/** + * Record a successful observation. Listeners must be synchronous recorders: + * throws fail the tool call and returned promises are not awaited. + * @param target - the target that was read/written/edited. + * @param version - the version the actor now holds as its observation. + * @param actor - the observing tool-execution context; undefined records nothing useful. + * @mode emit + */ 'fs/observed'(target: FsTarget, version: FsVersion, actor: object | undefined): void ``` @@ -226,6 +390,14 @@ Source: [`packages/fs/fs/src/index.ts:70`](../../packages/fs/fs/src/index.ts) Single-slot decision for the next FileSystem.writeText. Calling `next()` yields the bare provider's unconditional write; the first listener that returns an intent owns the decision rather than composing with peers. ```ts cordis-catalog +/** + * Single-slot decision for the next {@link FileSystem.writeText}. Calling + * `next()` yields the bare provider's unconditional write; the first listener + * that returns an intent owns the decision rather than composing with peers. + * @param target - the resolved target about to be written. + * @param actor - the opaque tool-execution context the decider keys off. + * @mode waterfall + */ 'fs/write-intent'(target: FsTarget, actor: object | undefined, next: () => FsWriteIntent | undefined | Promise): Promise ``` @@ -240,6 +412,17 @@ Source: [`packages/fs/fs/src/index.ts:53`](../../packages/fs/fs/src/index.ts) Waterfall around every streaming model call (retry, replay, routing). Bound to the LlmService; call `next()` to reach the resolved adapter's stream, or yield your own chunks to short-circuit. ```ts cordis-catalog +/** + * Waterfall around every streaming model call (retry, replay, routing). + * Bound to the {@link LlmService}; call `next()` to reach the resolved + * adapter's stream, or yield your own chunks to short-circuit. + * @param options - the full request. A LOOP-built request arrives + * deep-frozen (mutation throws): its content is a pure function of the + * session log (the reconstructability RFC), so listeners read it, never + * rewrite it. A hand-built one-shot (compaction summarize) is the + * caller's own object and stays mutable here. + * @mode waterfall + */ 'llm/stream'(this: LlmService, options: GenerateOptions, next: () => AsyncIterable): AsyncIterable ``` @@ -254,6 +437,17 @@ Source: [`packages/llm/llm/src/index.ts:40`](../../packages/llm/llm/src/index.ts Creation announcement during session publication. A synchronous throw vetoes and rolls back with a paired disposal; detach requested during dispatch is deferred. A returned-promise rejection is logged but cannot retroactively veto this synchronous boundary. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only sessions entered through that agent's context. ```ts cordis-catalog +/** + * Creation announcement during session publication. A synchronous throw vetoes and rolls + * back with a paired disposal; detach requested during dispatch is deferred. + * A returned-promise rejection is logged but cannot retroactively veto this + * synchronous boundary. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners + * receive only sessions entered through that agent's context. + * @param session - the session just entered and announced. + * @dshScopeScan unsupported + * @mode emit + */ 'session/created'(this: Scoped, session: Session): void ``` @@ -264,6 +458,15 @@ Source: [`packages/core/session/src/index.ts:47`](../../packages/core/session/sr Emitted once when an announced session leaves the store, including publication rollback, but never for an entry whose creation announcement did not begin. Listener failures are logged and contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. ```ts cordis-catalog +/** + * Emitted once when an announced session leaves the store, including + * publication rollback, but never for an entry whose creation announcement + * did not begin. Listener failures are logged and contained. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the owner scope. + * @param session - the session that is no longer live in the store. + * @dshScopeScan unsupported + * @mode emit + */ 'session/disposed'(this: Scoped, session: Session): void ``` @@ -274,6 +477,17 @@ Source: [`packages/core/session/src/index.ts:57`](../../packages/core/session/sr Post-commit, fire-and-forget append feed. The listener snapshot resolves before the log push, but callbacks run after it; observer failures are logged and contained without making the committed append fail. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only events from sessions entered through that agent's context. ```ts cordis-catalog +/** + * Post-commit, fire-and-forget append feed. The listener snapshot resolves + * before the log push, but callbacks run after it; observer failures are + * logged and contained without making the committed append fail. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners + * receive only events from sessions entered through that agent's context. + * @param session - the session whose log grew. + * @param event - the appended event, exactly as recorded. + * @dshScopeScan unsupported + * @mode emit + */ 'session/event'(this: Scoped, session: Session, event: SessionEvent): void ``` @@ -286,6 +500,15 @@ Source: [`packages/core/session/src/index.ts:69`](../../packages/core/session/sr Awaited parallel durability checkpoint: every listener runs and the caller awaits all of them, with no waterfall veto. Dispatch through SessionStore.flush. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. ```ts cordis-catalog +/** + * Awaited parallel durability checkpoint: every listener runs and the + * caller awaits all of them, with no waterfall veto. Dispatch through + * {@link SessionStore.flush}. Scope-filtered dispatch + * (`@deepseek-ai/dsh-scope`) reuses the session's owner scope. + * @param session - the session whose buffered events must reach durable storage. + * @dshScopeScan unsupported + * @mode parallel + */ 'session/flush'(this: Scoped, session: Session): Promise | void ``` @@ -298,6 +521,14 @@ Source: [`packages/core/session/src/index.ts:79`](../../packages/core/session/sr A ready child settled. Scope-filtered dispatch uses the same delegating parent carrier as `subagent/start`, so the lifecycle pair reaches the same scoped audience. ```ts cordis-catalog +/** + * A ready child settled. Scope-filtered dispatch uses the same delegating + * parent carrier as `subagent/start`, so the lifecycle pair reaches the + * same scoped audience. + * @param info - the run identity and terminal outcome. + * @dshScopeScan unsupported + * @mode emit + */ 'subagent/end'(this: Scoped, info: SubagentRunEndInfo): void ``` @@ -308,6 +539,11 @@ Source: [`packages/subagent/subagent/src/index.ts:112`](../../packages/subagent/ A provider became resolvable in the registry. ```ts cordis-catalog +/** + * A provider became resolvable in the registry. + * @param provider - the registered provider. + * @mode emit + */ 'subagent/provider-added'(provider: SubagentProvider): void ``` @@ -318,6 +554,11 @@ Source: [`packages/subagent/subagent/src/index.ts:86`](../../packages/subagent/s A provider left the registry. Accepted runs remain holder-owned. ```ts cordis-catalog +/** + * A provider left the registry. Accepted runs remain holder-owned. + * @param name - the provider name that no longer resolves. + * @mode emit + */ 'subagent/provider-removed'(name: string): void ``` @@ -328,6 +569,16 @@ Source: [`packages/subagent/subagent/src/index.ts:92`](../../packages/subagent/s A provider established a ready child. For in-process providers, `ctx.agents.get(info.id)` resolves during this notification. Scope-filtered dispatch keys the carrier by the delegating parent, so a parent-scoped listener observes only its own delegations. Paired with `subagent/end`. ```ts cordis-catalog +/** + * A provider established a ready child. For in-process providers, + * `ctx.agents.get(info.id)` resolves during this notification. + * Scope-filtered dispatch keys the carrier by the delegating parent, so a + * parent-scoped listener observes only its own delegations. Paired with + * `subagent/end`. + * @param info - the provider and ready child identity. + * @dshScopeScan unsupported + * @mode emit + */ 'subagent/start'(this: Scoped, info: SubagentRunInfo): void ``` @@ -340,6 +591,14 @@ Source: [`packages/subagent/subagent/src/index.ts:103`](../../packages/subagent/ Expert waterfall over the assembled sections, tools, and variables. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners receive only that scope's assemblies. The returned value is authoritative. ```ts cordis-catalog +/** + * Expert waterfall over the assembled sections, tools, and variables. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): scoped listeners + * receive only that scope's assemblies. The returned value is authoritative. + * @param assembly - the mutable assembly built from registered providers. + * @param context - the caller's per-assembly context. + * @mode waterfall + */ 'system-prompt/assemble'(this: Scoped, assembly: PromptAssembly, context: AssembleContext, next: () => Promise): Promise ``` @@ -350,6 +609,11 @@ Source: [`packages/core/system-prompt/src/index.ts:27`](../../packages/core/syst Emitted when any prompt provider changes. This registry notification is unfiltered because a global change affects every scope. ```ts cordis-catalog +/** + * Emitted when any prompt provider changes. This registry notification is + * unfiltered because a global change affects every scope. + * @mode emit + */ 'system-prompt/change'(): void ``` @@ -362,6 +626,15 @@ Source: [`packages/core/system-prompt/src/index.ts:33`](../../packages/core/syst A tool was registered or unregistered, or a scoped restriction changed (the available tool set changed — possibly for one scope only). An UNFILTERED registry-subject notification, deliberately not scope-filtered dispatch: a global change concerns every agent's next assembly, so a scoped listener subscribing here sees every change, not just its own scope's. ```ts cordis-catalog +/** + * A tool was registered or unregistered, or a scoped restriction changed + * (the available tool set changed — possibly for one scope only). An + * UNFILTERED registry-subject notification, deliberately not scope-filtered + * dispatch: a global change concerns every agent's next assembly, so a + * scoped listener subscribing here sees every change, not just its own + * scope's. + * @mode emit + */ 'tools/change'(): void ``` @@ -372,6 +645,14 @@ Source: [`packages/core/tools/src/index.ts:116`](../../packages/core/tools/src/i Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns a normalized result; wrappers may change only `exec.signal`, while call identity remains immutable. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. ```ts cordis-catalog +/** + * Around-dispatch waterfall for timeout, retry, or metrics. `next()` returns + * a normalized result; wrappers may change only `exec.signal`, while call + * identity remains immutable. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the allowed call about to dispatch (name, parsed arguments, caller agent, signal). + * @mode waterfall + */ 'tools/execute'(this: Scoped, exec: ToolExecution, next: () => Promise): Promise ``` @@ -384,6 +665,14 @@ Source: [`packages/core/tools/src/index.ts:89`](../../packages/core/tools/src/in Accept, replace, enrich, or block a normalized dispatch result. `next()` accepts it unchanged; thrown tools still reach this seam as errors. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. ```ts cordis-catalog +/** + * Accept, replace, enrich, or block a normalized dispatch result. `next()` + * accepts it unchanged; thrown tools still reach this seam as errors. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the call that just ran (name, parsed arguments, caller agent). + * @param result - the dispatch outcome a listener may accept, replace, or block. + * @mode waterfall + */ 'tools/post-execute'(this: Scoped, exec: ToolExecution, result: Readonly, next: () => Promise): Promise ``` @@ -396,6 +685,13 @@ Source: [`packages/core/tools/src/index.ts:98`](../../packages/core/tools/src/in Allow, deny, or ask before dispatch. `next()` delegates to allow; missing approval support turns `ask` into denial. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. ```ts cordis-catalog +/** + * Allow, deny, or ask before dispatch. `next()` delegates to allow; missing + * approval support turns `ask` into denial. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners receive only that agent's calls. + * @param exec - the pending call (name, parsed arguments, caller agent). + * @mode waterfall + */ 'tools/pre-execute'(this: Scoped, exec: ToolExecution, next: () => Promise): Promise ``` @@ -408,6 +704,13 @@ Source: [`packages/core/tools/src/index.ts:80`](../../packages/core/tools/src/in Observe the frozen, lossless-JSON final outcome. Listener failures are contained. Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. ```ts cordis-catalog +/** + * Observe the frozen, lossless-JSON final outcome. Listener failures are contained. + * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): keyed by `exec.agent`. + * @param exec - the execution object that traversed the pipeline. + * @param result - a deep-frozen snapshot of the final returned result. + * @mode emit + */ 'tools/result'(this: Scoped, exec: Readonly, result: Readonly): undefined ``` @@ -422,6 +725,16 @@ Source: [`packages/core/tools/src/index.ts:106`](../../packages/core/tools/src/i One `agent()` call settled (clean result, child failure, or run cancellation). Paired with Events['workflow/agent-start'] by `agent.seq`, exactly once per started call on every stop path — on an engine termination path (a worker killed past its grace) the end is engine-synthesized with outcome `'cancelled'`. ```ts cordis-catalog +/** + * One `agent()` call settled (clean result, child failure, or run + * cancellation). Paired with {@link Events['workflow/agent-start']} by + * `agent.seq`, exactly once per started call on every stop path — on an + * engine termination path (a worker killed past its grace) the end is + * engine-synthesized with outcome `'cancelled'`. + * @param info - the run's identity snapshot. + * @param agent - the call identity plus its outcome. + * @mode emit + */ 'workflow/agent-end'(info: WorkflowRunInfo, agent: WorkflowAgentEndInfo): void ``` @@ -432,6 +745,15 @@ Source: [`packages/workflow/workflow/src/index.ts:81`](../../packages/workflow/w One `agent()` call established a ready child run. Paired with Events['workflow/agent-end'] by `agent.seq`. A call that never receives a ready run from the provider emits neither event in this pair. ```ts cordis-catalog +/** + * One `agent()` call established a ready child run. Paired with + * {@link Events['workflow/agent-end']} by `agent.seq`. A call that never + * receives a ready run from the provider emits neither + * event in this pair. + * @param info - the run's identity snapshot. + * @param agent - the call's sequence number, label, phase, and child id. + * @mode emit + */ 'workflow/agent-start'(info: WorkflowRunInfo, agent: WorkflowAgentInfo): void ``` @@ -442,6 +764,15 @@ Source: [`packages/workflow/workflow/src/index.ts:70`](../../packages/workflow/w A workflow run settled (any stop reason). Fired when WorkflowRun.result resolves. Paired with Events['workflow/start']. ```ts cordis-catalog +/** + * A workflow run settled (any stop reason). Fired when + * {@link WorkflowRun.result} resolves. Paired with + * {@link Events['workflow/start']}. + * @param info - the run's identity snapshot. + * @param result - the outcome data (stop reason, error, agent count) — + * deliberately WITHOUT the result value (see {@link WorkflowResultInfo}). + * @mode emit + */ 'workflow/end'(info: WorkflowRunInfo, result: WorkflowResultInfo): void ``` @@ -452,6 +783,12 @@ Source: [`packages/workflow/workflow/src/index.ts:91`](../../packages/workflow/w The script emitted a narration line (a `log(message)` call). ```ts cordis-catalog +/** + * The script emitted a narration line (a `log(message)` call). + * @param info - the run's identity snapshot. + * @param message - the logged message, verbatim. + * @mode emit + */ 'workflow/log'(info: WorkflowRunInfo, message: string): void ``` @@ -462,6 +799,13 @@ Source: [`packages/workflow/workflow/src/index.ts:60`](../../packages/workflow/w The script entered a phase (a `phase(title)` call) — progress grouping for observers; no execution semantics. ```ts cordis-catalog +/** + * The script entered a phase (a `phase(title)` call) — progress grouping + * for observers; no execution semantics. + * @param info - the run's identity snapshot. + * @param title - the phase title, verbatim. + * @mode emit + */ 'workflow/phase'(info: WorkflowRunInfo, title: string): void ``` @@ -472,6 +816,12 @@ Source: [`packages/workflow/workflow/src/index.ts:53`](../../packages/workflow/w A workflow run started — the script's meta block validated, the body about to execute. Paired with Events['workflow/end']. ```ts cordis-catalog +/** + * A workflow run started — the script's meta block validated, the body + * about to execute. Paired with {@link Events['workflow/end']}. + * @param info - the run's identity snapshot (id + meta). + * @mode emit + */ 'workflow/start'(info: WorkflowRunInfo): void ``` diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index 40bd029e41..22d4565830 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -3,66 +3,226 @@ # Cordis Services Catalog -Every `ctx.` service a plugin can call: the exact public interface plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against. +Every `ctx.` service a plugin can call: the exact public interface with original method JSDoc, plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against. -This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence (skipped by doc-typecheck, since a bare signature is not standalone-compilable). Type names in a signature link to the page that documents them. +This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them. The **harness tier** below (the `@deepseek-ai/dsh-*` packages) is the vocabulary this repo owns. The **inherited tier** at the end is the cordis-core + loader/hmr/timer `ctx` surface a plugin also sees — pinned vendor source, summarized tersely. -## `ctx.agentExecution` — `AgentExecutionService` (abstract seam) - -Ambient Agent identity within one process-local asynchronous chain. - -```ts cordis-catalog -current(): AgentExecution | undefined -require(): AgentExecution -run(execution: AgentExecution | undefined, operation: () => T): T -``` - -Types: [AgentExecution](../core-data-structures/core.md) - -Source: [`packages/core/agent-execution/src/index.ts:18`](../../packages/core/agent-execution/src/index.ts) - ## `ctx.agentLoop` — `AgentLoop` Concrete agent factory and driver service. ```ts cordis-catalog +/** + * Create an agent and session under one caller-supplied identity, owned by + * the accessing fiber. Constructor-driven config calls mint a fresh combined + * id before entering this boundary. + * @param id - shared agent/session identity. + * @param options - concrete loop options. + * @param meta - optional fresh-session workspace metadata. + * @returns the published running agent. + */ create(id: SessionId, options: AgentOptions = {}, meta: Pick = {}): Agent + +/** + * Create an owned agent on a caller-supplied session id. + * @param ownerCtx - caller context that structurally owns the transaction. + * @param options - identities, session seed/metadata, loop options, setup, and cancellation. + * @returns the published handle. + */ async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise + +/** + * Resume an owned agent from the configured persistence service. + * @param ownerCtx - caller context that owns load, setup, and the live lifecycle. + * @param options - persisted identity, loop options, setup, and cancellation. + * @returns the published handle. + */ async resume(ownerCtx: Context, options: ResumeAgentOptions): Promise ``` Types: [Agent](../core-data-structures/core.md) -Source: [`packages/core/agent-loop/src/index.ts:408`](../../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:407`](../../packages/core/agent-loop/src/index.ts) ## `ctx.agents` — `AgentRegistry` -Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory. +Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory. ```ts cordis-catalog +/** + * Read the Agent that initiated the inherited asynchronous driver chain. + * @returns the inherited Agent, or `undefined` outside a driver and inside an explicit clearing boundary. + * @throws when this service instance has been disposed. + */ +currentInitiator(): Agent | undefined + +/** + * Read the initiating Agent and fail when no driver boundary is active. + * @returns the inherited Agent. + * @throws when no initiator is active or this service instance has been disposed. + */ +requireInitiator(): Agent + +/** + * Run an operation with one exact Agent as its process-local initiator. The + * exact synchronous value or Promise returned by the operation is preserved. + * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization. + * @param operation - synchronous or asynchronous operation to invoke. + * @returns the exact value returned by `operation`. + * @throws when the initiator scope is closing/disposed, or when `operation` throws. + */ +withInitiator(agent: Agent, operation: () => T): T + +/** + * Run an operation inside a boundary that hides any inherited initiating + * Agent. The exact synchronous value or Promise is preserved. + * @param operation - synchronous or asynchronous operation to invoke without an initiator. + * @returns the exact value returned by `operation`. + * @throws when the initiator scope is closing/disposed, or when `operation` throws. + */ +withoutInitiator(operation: () => T): T + +/** + * Register the agent-creation factory (the loop calls this on construction, + * effect-scoped). A traced Cordis service is canonicalized to its concrete + * target; each create/resume call is then traced through that caller's + * context so ownership follows the caller without stacking proxy layers. + * Throws if a factory is already registered. Returns the disposer; on + * dispose the factory slot is cleared. + * @param factory - the loop-owned factory {@link create}/{@link resume} delegate to. + * @returns the disposer that clears the factory slot. The exact + * Cordis effect disposer (single-shot): composite (generator) effects may + * yield it directly — exact identity nests the teardown in order. + */ setFactory(factory: AgentFactory): () => void + +/** + * Create and publish a new agent through the registered factory. + * Distinct from {@link register} (which records an already-constructed + * agent): this constructs the agent and its session. Rejects if no factory is + * registered or creation/setup fails. The resolved {@link AgentHandle} lets + * the owner tear down exactly this agent. + * @param options - shared identity, session seed/metadata, and agent options. + * @returns the handle after setup, rollback-covered publication, and loop start complete. + */ async create(options: CreateAgentOptions): Promise + +/** + * Load a persisted session and resume an agent on it through the registered + * factory. Rejects if no factory is registered; the factory rejects if + * session persistence is not configured or persistence/setup fails. + * @param options - persisted identity, configuration, and optional setup. + * @returns the handle after setup, rollback-covered publication, and loop start complete. + */ async resume(options: ResumeAgentOptions): Promise + +/** + * Register a live agent. Throws if an agent with the same id is already + * registered. Emits `agent/created` on registration and `agent/disposed` + * when the calling fiber is disposed — both with the agent's scope carrier + * (`scopeTarget(agent, agent)`): the subject is the agent in hand, so the + * emits are scope-filtered regardless of which context invoked `register` + * (calling through `agent.ctx` scopes EFFECTS; dispatch scoping always + * requires passing the carrier). Returns the disposer. + * @param agent - the already-constructed agent to record in the store. + * @returns the EXACT Cordis effect disposer (single-shot; a repeat call + * returns undefined without awaiting an in-flight teardown). Exact + * identity is load-bearing: a composite (generator) effect that owns a + * teardown ORDER — the agent factory's lifecycle chain — must yield THIS + * function so Cordis nests the unregistration at that yield position; + * yielding a wrapper would leave it disposing as a concurrent sibling on + * owner unload, unregistering the agent (and emitting `agent/disposed`) + * while its final turn is still draining. + */ register(agent: Agent): () => void + +/** + * Insert an already-constructed agent without announcing it. This is the + * advanced ordered-lifecycle primitive used by the async agent factory: it + * first completes setup while the agent is unpublished, then assigns the + * returned detach closure into its pre-installed composite teardown before + * calling {@link announce}. Ordinary callers use {@link register}. + * @param agent - the prepared, unpublished agent. + * @param owner - live agent whose scoped context created this agent, or + * undefined for a top-level runtime root. This is runtime ownership, not + * the resumed session's durable parent lineage. + * @returns an idempotent closure that removes this exact entry and emits + * `agent/disposed` with listener failures contained. When called from a + * synchronous `agent/created` listener, removal and disposal wait until + * that creation dispatch unwinds. + */ enter(agent: Agent, owner: Agent | undefined): () => void + +/** + * Announce an agent previously inserted with {@link enter}. + * @param agent - the live inserted agent to announce. + * @throws if `agent` is not the exact live registry entry for its id, or its + * creation announcement already began (including a reentrant call from a + * creation listener). + */ announce(agent: Agent): void + +/** + * Look up a live agent. + * @param id - the shared agent/session id to look up. + * @returns the agent, or undefined when no live agent has that id. + */ get(id: SessionId): Agent | undefined + +/** + * Test whether a live agent was created through one exact parent agent's + * scoped context. Runtime ownership is independent of durable session + * lineage and remains unambiguous when unrelated providers reuse an id. + * @param id - the candidate child agent's shared agent/session id. + * @param owner - the expected runtime creator agent. + * @returns true only while the exact child entry is live under that owner. + */ isOwnedBy(id: SessionId, owner: Agent): boolean + +/** + * All live agents, in registration order. + * @returns a fresh array; mutating it does not affect the registry. + */ list(): Agent[] + +/** + * All live top-level agents in registration order. A top-level agent was + * created without an owning agent context; durable session lineage does not + * affect this runtime relation, so a resumed fork may still be a root. + * @returns a fresh array; mutating it does not affect the registry. + */ roots(): Agent[] ``` Types: [Agent](../core-data-structures/core.md) -Source: [`packages/core/agent/src/index.ts:201`](../../packages/core/agent/src/index.ts) +Source: [`packages/core/agent/src/index.ts:204`](../../packages/core/agent/src/index.ts) ## `ctx.approval` — `ApprovalService` Approval service that applies session policy before answerers and logs every ask/outcome pair to the requesting session. It exposes deterministic policy changes to the model through prompt and pre-step notices. ```ts cordis-catalog +/** + * Ask the composed answerers to decide one readonly same-process request. + * The service borrows the request, agent, session, and live signal directly. + * The request requires an open turn because the audit pair must be enclosed + * by the durable log's commit/replay boundary; an idle ask rejects before + * appending anything. The answerer phase always produces an outcome: an + * aborted signal yields `'cancelled'`, a missing or throwing answerer yields + * `'unavailable'` (fail closed), and a rogue non-vocabulary return value is + * normalized to `'unavailable'`. A failure that prevents either audit append + * from committing still rejects because returning an unlogged decision would + * violate the pair. Session contains post-commit observer failures, so an + * authoritative append cannot reject the request or suppress its matching + * audit event. + * @param req - the pending decision (agent, tool identity, reason, signal). + * @returns the closed outcome; `'allowed-once'` is the only grant. + * @throws when no turn is open or either audit event fails before the session + * append commit point. + */ async request(req: ApprovalRequest): Promise ``` @@ -82,8 +242,27 @@ Implementations must honor these semantics: - Disposal kills all running background processes and awaits their exit. ```ts cordis-catalog +/** + * Apply implementation-owned defaults and caps to a request before execution. + * @param request - the caller's request; omitted fields get this + * implementation's defaults, capped fields are clamped. + * @returns the fully-specified spec to hand to {@link run}/{@link start}. + */ abstract resolve(request: BashExecRequest): BashExecSpec + +/** + * Run a command in the foreground; resolves when it finishes. + * @param spec - a resolved spec from {@link resolve}, never a raw request. + * @returns the outcome; nonzero exits, timeout kills, and abort kills + * resolve with a descriptive result rather than reject. + */ abstract run(spec: BashExecSpec): Promise + +/** + * Start a background process and return its handle immediately. + * @param spec - a resolved spec from {@link resolve}, never a raw request. + * @returns the live process handle (reads, kill, quiescence promise). + */ abstract start(spec: BashExecSpec): BashProcess ``` @@ -96,8 +275,25 @@ Source: [`packages/bash/bash/src/index.ts:49`](../../packages/bash/bash/src/inde Registry (`ctx.bashEnv`) for trusted, per-execution `DSH_*` variables. The namespace is rebuilt for every model bash call: ambient `DSH_*` values are discarded by the executor, then the registry's current snapshot is injected. Built-in shell facts remain owned by the registry itself while plugins can register additional, enumerable facts with effect-scoped disposal. ```ts cordis-catalog +/** + * Register one environment contributor. Names and keys are unique; built-in + * keys are reserved. Registration is disposed with the calling plugin fiber. + * @param contributor - declared key ownership and per-execution resolver. + * @returns the disposer that unregisters the contribution. + */ register(contributor: BashEnvContributor): () => void + +/** + * Build the trusted `DSH_*` snapshot for one bash tool execution. + * @param execution - the current tool execution. + * @returns an immutable environment overlay containing built-ins and current contributions. + */ collect(execution: ToolExecution): DshEnvironment + +/** + * Enumerate plugin-contributed variables without executing their resolvers. + * @returns declarations sorted by environment variable name. + */ list(): BashEnvVariableInfo[] ``` @@ -110,6 +306,15 @@ Source: [`packages/bash/tool-bash/src/index.ts:102`](../../packages/bash/tool-ba Registers one `ctx.codeRuntime` implementation. Program, budget, abort, and substrate failures resolve in CodeRunResult; only seam misuse rejects. Implementations bridge structured-cloneable bindings while treating programs as hostile peers, isolate runs from one another, and terminate and await in-flight runs during disposal. ```ts cordis-catalog +/** + * Execute one program against the request's bindings and capture what it + * emitted. See the class doc for the resolution contract (error is a result + * field; rejection means seam misuse only). + * @param request - the program, its bindings, and the abort signal; the + * request carries everything the runtime acts on, with no hidden defaults. + * @returns the run's outcome: completion value (when transferable), the + * ordered log capture, and the failure (if any). + */ abstract run(request: CodeRunRequest): Promise ``` @@ -122,7 +327,40 @@ Source: [`packages/code-runtime/code-runtime/src/index.ts:30`](../../packages/co Abstract compaction service. Implementations own trigger policy, retention, and summarization, and may consume a separate measurement service. A successful run replaces the selected surface span with one summary node and prevents concurrent compaction of the same session. Load one implementation per context as `ctx.compact`. ```ts cordis-catalog +/** + * Check token pressure and compact if the conversation is too large. + * Estimate the next request, including its session prefix, derived history, + * and system prompt. Above threshold, compact a head-anchored range ending at + * a balanced tool boundary and reconsolidate any prior automatic checkpoint. + * Return `null` when no compaction is needed or an open tail leaves no safe + * cutoff. A single oversized retained unit or prefix cannot be repaired here. + * + * @param agent - agent context owning the session surface and model options. + * @param fullSystemPrompt - assembled system prompt, counted toward the estimate. + * @param sessionPrefix - the instance's composed session prefix, counted toward the + * estimate. + * @param signal - cancellation signal; model-backed implementations must forward it. + * @returns the compaction result, or `null` if no compaction was needed. + */ abstract compactIfNeeded( agent: CompactAgentContext, fullSystemPrompt: string, sessionPrefix: readonly Message[], signal: AbortSignal, ): Promise + +/** + * Forcibly compact a range of surface nodes into a single summary node. + * `start` and `end` name an inclusive span by surface position, not numeric seq + * order; replacements can make visible seqs non-monotonic. Both edges must be + * balanced so assistant tool calls remain paired with their results. A model- + * backed implementation forwards cancellation and rejects active, missing, + * reversed, or unbalanced ranges. The target session is `agent.session`. + * Use {@link toolPairingBalancedBefore} and {@link toolPairingBalancedAfter} + * for the edge checks. + * + * @param start - first surface seq, inclusive. + * @param end - last surface seq, inclusive. + * @param agent - context whose session is mutated and whose routing options guide summarization. + * @param signal - optional cancellation; model-backed implementations must forward it. + * @throws when compaction is active or the range is missing, reversed, or unbalanced. + * @returns the appended event seqs, summary, replaced range, and token accounting. + */ abstract compactRegion( start: number, end: number, agent: CompactAgentContext, signal?: AbortSignal, ): Promise ``` @@ -135,13 +373,90 @@ Source: [`packages/compact/compact/src/index.ts:38`](../../packages/compact/comp Abstract filesystem provider. Targets must preserve identity across aliases; reads expose regular UTF-8 text or typed errors, listings are stable and content-free, and mutations are atomic. Optional guards add stale protection without changing the unguarded provider contract. ```ts cordis-catalog +/** + * Resolve a model/plugin-supplied path into a stable {@link FsTarget}. May perform I/O (a + * remote/sandboxed backend may need a round-trip to map a path to a stable identity), hence + * async even though the local backend only normalizes + realpaths. + * + * @param path - the path to resolve; relative paths resolve against `opts.cwd`. + * @param opts - optional cwd override and cancellation signal. + * @returns the stable target; the same file yields the same `targetKey`. + */ abstract resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise + +/** + * Return target metadata, or `undefined` when the target does not exist. + * @param target - the resolved target to stat. + * @param signal - aborts the metadata round-trip. + * @returns metadata only, never content; undefined for an absent target. + */ abstract stat(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Return path metadata without following the final path component when it is a + * symbolic link. This is intentionally path-shaped, not target-shaped: + * {@link resolve} follows symlinks to produce the stable identity used by + * normal reads/writes, while `lstat` lets a consumer reject the path itself + * before that follow happens. + * + * `opts.cwd` follows {@link resolve}'s cwd rules. `undefined` means the path is + * absent. + * @param path - the path to inspect; relative paths resolve against `opts.cwd`. + * @param opts - `cwd` overrides the backend's default base for relative paths. + * @param signal - aborts the metadata round-trip. + * @returns metadata only, never content; undefined for an absent path. + */ abstract lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise + +/** + * Read the whole regular text file as a single decoded string. + * @param target - the resolved target to read. + * @param signal - aborts the read. + * @returns the full decoded UTF-8 content. + */ abstract readText(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Stream the whole regular text file as decoded text chunks (same text + * semantics as {@link readText}, for large files). The backend owns + * cross-chunk UTF-8 decoding and binary rejection so the policy layer never + * touches raw bytes. + * @param target - the resolved target to read. + * @param signal - aborts the stream, including between chunks. + * @returns the chunk iterable, decoded and validated like {@link readText}. + */ abstract streamText(target: FsTarget, signal?: AbortSignal): Promise> + +/** + * List direct children of a directory in stable name order. Returns resolved + * child targets plus cheap metadata only; never reads file contents. + * @param target - the resolved directory target. + * @param signal - aborts the listing. + * @returns one entry per direct child, in stable name order. + */ abstract listDir(target: FsTarget, signal?: AbortSignal): Promise + +/** + * Atomically create or replace UTF-8 text. `expected` guards intent and + * staleness; omission allows unconditional overwrite. + * @param target - the resolved target to write. + * @param content - the full new file content. + * @param expected - the write intent guarding the write; omit for unconditional. + * @param signal - aborts before the atomic rename takes effect. + * @returns the outcome, including the version the write produced. + */ abstract writeText(target: FsTarget, content: string, expected?: FsWriteIntent, signal?: AbortSignal): Promise + +/** + * Atomically edit literal text. When supplied, the version guard is checked + * before matching so stale content reports `FS_STALE_VERSION`; omission edits + * the current content without a freshness precondition. + * @param target - the resolved target to edit. + * @param edit - the literal search/replace request. + * @param expected - the version guard; omit for an unconditional edit. + * @param signal - aborts before the atomic rename takes effect. + * @returns the outcome, including the version the edit produced. + */ abstract editText(target: FsTarget, edit: FsEditRequest, expected?: { version: FsVersion }, signal?: AbortSignal): Promise ``` @@ -154,9 +469,39 @@ Source: [`packages/fs/fs/src/index.ts:80`](../../packages/fs/fs/src/index.ts) The abstract `llm` service: an adapter registry plus a streaming model-call surface, interceptable via the `llm/stream` waterfall. ```ts cordis-catalog +/** + * Register an adapter for the given provider routes. Throws `LlmError` with code + * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing). + * Disposed with the fiber. + * @param providers - every provider route this adapter should serve. + * @param adapter - the adapter that streams calls for those providers. + * @returns the disposer that unregisters all of them. + */ registerAdapter(providers: string[], adapter: LlmAdapter): () => void + +/** + * Describe provider routes with a registered adapter. + * @returns detached provider metadata in registration order. + */ listProviders(): LlmProviderInfo[] + +/** + * Discover models advertised by one registered provider. Catalog membership + * is advisory and never changes routing or request validation. + * @param provider - registered provider route to inspect. + * @returns detached model metadata in adapter-preferred order. + */ async listModels(provider: string): Promise + +/** + * Stream one model call as raw chunks (token-level deltas). Throws + * `LlmError` with code `NO_ADAPTER` if no adapter is registered for + * `options.provider`. Replay state is retained only when the same adapter + * instance owns its historical provider and the target provider. Dispatches + * through the `llm/stream` waterfall. + * @param options - the full request; `options.provider` selects the adapter. + * @returns the chunk stream, possibly wrapped by `llm/stream` listeners. + */ stream(options: GenerateOptions): AsyncIterable ``` @@ -169,9 +514,38 @@ Source: [`packages/llm/llm/src/index.ts:94`](../../packages/llm/llm/src/index.ts Owns the deployment's permission presets and their write path. Requires a confining `ctx.bash` executor and `ctx.approval`; unmatched knob values are reported as CUSTOM_PRESET, not an error. ```ts cordis-catalog +/** + * Resolve the preset matching the effective knob values. A still-matching + * last selection wins shared-bundle ties; otherwise the first table match + * wins, or {@link CUSTOM_PRESET} when no entry matches. + * @param events - the session's events in log order. + * @returns the effective preset name, or `custom` when nothing matches. + */ current(events: readonly SessionEvent[]): string + +/** + * Resolve a preset's knob bundle. + * @param name - the preset name to resolve. + * @returns the configured bundle. + * @throws when `name` is not in the table. + */ resolve(name: string): PresetSpec + +/** + * Build the client option for a table entry or {@link CUSTOM_PRESET}. A + * missing label falls back to the table key. + * @param name - a table key, or `custom`. + * @returns the option a client renders. + * @throws when `name` is neither a table key nor `custom`. + */ optionOf(name: string): PresetOption + +/** + * Record a changed preset, then update each changed knob through its own + * setter. Selecting the effective preset again appends nothing. + * @param session - the session the switch belongs to. + * @param name - the preset to switch to; unknown names throw. + */ set(session: Session, name: string): void ``` @@ -184,6 +558,17 @@ Source: [`packages/ui/permission/src/index.ts:94`](../../packages/ui/permission/ Abstract process-sandbox service. confine must return enforcing argv or fail closed at wrap or runner-execution time; silent unconfined passthrough is forbidden. Functional probes arbitrate multi-runner chains and may be skipped for a sole candidate, whose own refusal remains the fail-closed end. ```ts cordis-catalog +/** + * Wrap `argv` so it executes confined under `policy` on this host; the + * caller spawns the returned argv in place of its own. + * @param argv - the exact argv the caller is about to spawn (program plus + * arguments), NOT a shell string — a shell-shaped consumer passes + * `['bash', '-c', command]`. + * @param policy - the file-effect policy this execution runs under, + * carried per call (see {@link SandboxPolicy}). + * @returns the argv to spawn instead, plus the enforcement completeness + * the selected backend achieves for it. + */ abstract confine(argv: readonly string[], policy: SandboxPolicy): ConfinedArgv ``` @@ -196,10 +581,49 @@ Source: [`packages/sandbox/sandbox/src/index.ts:111`](../../packages/sandbox/san Durable append-only session storage. Implementations preserve contiguous, losslessly JSON-serializable events; append resolves only after durability, and load balances a complete interrupted tail without rewriting committed events. ```ts cordis-catalog +/** + * Resolve this backend's independent local artifact for a session without + * reading, creating, flushing, or otherwise materializing it. Backends such + * as SQLite that do not own one artifact per session return `undefined`. + * @param meta - the immutable session header whose artifact is requested. + * @returns the backend-specific absolute location, when one exists. + */ abstract locate(meta: SessionHeader): SessionLocation | undefined + +/** + * Register a new session's metadata. A backend MAY defer the physical write + * until the first {@link append} (lazy materialization), in which case a + * created-but-never-appended session is absent from {@link list} + * — abandoned sessions leave nothing behind. + * @param meta - the immutable header (id, version, cwd, lineage) to record. + */ abstract create(meta: SessionHeader): Promise + +/** + * Durably persist a batch of events (called from the write-behind drain at + * the `session/flush` checkpoint). Honors the append-only and contiguous-seq + * contracts: the first event's `seq` MUST equal the stored next-seq (after + * `load` has durably closed any interrupted turn). Rejects non-JSON- + * serializable `event.data` with an error naming the offending event type. + * @param id - the session the batch belongs to. + * @param events - the contiguous batch to persist, in seq order. + */ abstract append(id: SessionId, events: readonly SessionEvent[]): Promise + +/** + * Load a header and balanced contiguous log. A complete interrupted final + * turn is preserved and durably closed with missing tool errors plus any open + * step and turn boundaries; only a torn final record is discarded. Unknown + * versions and corruption in the committed prefix reject. + * @param id - the persisted session to reload. + * @returns the header and a log ending on a balanced `turn/end`. + */ abstract load(id: SessionId): Promise<{ meta: SessionHeader; events: SessionEvent[] }> + +/** + * Lightweight listing from metadata, without a full-log parse. + * @returns one header per materialized session. + */ abstract list(): Promise ``` @@ -212,10 +636,40 @@ Source: [`packages/session-persistence/session-persistence/src/index.ts:42`](../ Live-preferred logical-corpus exact-read and relationship-tracing service. ```ts cordis-catalog +/** + * List the complete logical corpus using live-preferred records. + * @returns deterministic newest-first cloned session records. + */ listSessions(): Promise + +/** + * List lightweight raw-log event records for one logical session. + * @param sessionId - live-preferred session id to read. + * @returns event records in ascending seq order. + */ async listEvents(sessionId: SessionId): Promise + +/** + * Trace known ancestry and descendants from one corpus observation. + * @param sessionId - logical session id to trace. + * @returns a complete lineage or an explicit unresolved parent boundary. + * @throws when corpus resolution fails, the target is absent, or its known ancestry cycles. + */ async traceSession(sessionId: SessionId): Promise + +/** + * Trace one event's direct positional and provenance relationships. + * @param request - target session id and event seq. + * @returns direct links plus the target's positional replacement chain. + * @throws when source resolution fails, the target is absent, or surface/provenance validation fails. + */ async traceEvent(request: SessionEventTraceRequest): Promise + +/** + * Read one full event plus a bounded raw-log context window. + * @param request - target session/seq and context sizes. + * @returns cloned target and neighboring events. + */ async readEvent(request: SessionEventReadRequest): Promise ``` @@ -228,13 +682,119 @@ In-memory session store (`ctx.sessions`). Persistence is intentionally not implemented here — persistence plugins subscribe to `session/event` and flush on `session/flush` / dispose. ```ts cordis-catalog +/** + * Create a session owned by the calling fiber: disposing that fiber stops + * event notification and removes the session from the store. `options.seed` + * populates the session with a copy of those events (replay/fork); + * `options.meta` attaches creation metadata (validated absolute `cwd`, + * `parentSession` lineage) as the immutable {@link SessionHeader} (the store + * fills `version`/`id`/`createdAt`). + * + * For an agent whose session must be torn down IN ORDER with its loop (so the + * loop's final flush is captured before the store attachment ends), do NOT use this + * — fold the session lifecycle into the agent's own effect via + * {@link prepare} + {@link enter} + {@link announce} (see + * `dsh-agent-loop`'s creation transaction). + * + * @param id - the session id; omitted, the store mints `session-`. + * @param options - seed events and/or creation metadata for the header. + * @returns the live session, already entered and announced. + * @throws if a session with `id` already exists, metadata is not a plain + * lossless-JSON record with valid scalar fields, or `meta.cwd` is a + * non-absolute path (storage backends key directories off it). + */ create(id?: SessionId, options?: CreateSessionOptions): Session + +/** + * Build a session WITHOUT entering it into the store — validate the id/cwd and + * construct the {@link Session} (with its immutable {@link SessionHeader}). + * Pairs with {@link enter} + {@link announce}: a caller that owns a composite + * `ctx.effect` (the agent factory) folds the session lifecycle into that ONE + * effect so a fiber unload tears the session + agent down as a single ORDERED + * chain rather than as racing sibling effects — which would remove the publication hooks + * before the loop's closing `session/flush`, dropping the closing events. + * + * @param id - the session id; omitted, the store mints `session-`. + * @param options - seed events and/or creation metadata for the header. + * @returns the constructed session, NOT yet in the store. + * @throws if a session with `id` already exists, metadata is not a plain + * lossless-JSON record with valid scalar fields, or `meta.cwd` is a + * non-absolute path. + */ prepare(id?: SessionId, options?: CreateSessionOptions): Session + +/** + * Enter a {@link prepare}d session into the store: install the module-private + * append publication hooks and add it to the store. Returns the DETACH + * disposer (hooks + store removal). Does NOT emit `session/created` — + * the caller yields this disposer inside its effect and THEN calls + * {@link announce}, so a throwing `session/created` listener rolls the attach + * back instead of leaking it. + * + * Re-checks the id for a duplicate: `prepare` and `enter` are public + * cross-package primitives and a caller may interleave arbitrary work (or + * another create) between them, so a stale prepared session must NOT overwrite + * a live store entry of the same id — its detach disposer would later delete + * the REAL session. The {@link create} convenience and the agent factory call + * the two back-to-back so they never trip this, but the public seam cannot + * assume that. + * + * @param session - a {@link prepare}d session not yet in the store. + * @returns the detach disposer (publication hooks + store removal). When called from + * a synchronous `session/created` listener, removal and disposal wait until + * that creation dispatch unwinds. + * @throws if a session with this id is already in the store. + */ enter(session: Session): () => void + +/** Emit `session/created` exactly once for an {@link enter}ed session (with + * the carrier {@link enter} captured). Separate from {@link enter} so the + * caller can yield the detach disposer first (rollback safety — see + * {@link enter}). + * @param session - the entered session to announce to listeners. + * @throws if the session is not live or its announcement already began, + * including a reentrant call from a creation listener. */ announce(session: Session): void + +/** + * Dispatch the awaited `session/flush` durability checkpoint for `session`, + * with the carrier captured at {@link enter}. THE flush entry point: the + * store owns the carrier, so callers (the loop's turn-end checkpoint, idle + * injection, teardown drains) must come through here rather than dispatch a + * raw `ctx.parallel('session/flush', …)` — one owner, one spelling, and the + * scoped-dispatch invariant can pin it. + * @param session - the session whose buffered events must reach durable storage. + * @returns resolves when every flush listener has settled; after all settle, + * rejects with the first registered listener failure if any listener failed. + */ async flush(session: Session): Promise + +/** + * Look up a live session. + * @param id - the session id to look up. + * @returns the session, or undefined when no live session has that id. + */ get(id: SessionId): Session | undefined + +/** + * All live sessions, in creation order. + * @returns a fresh array; mutating it does not affect the store. + */ list(): Session[] + +/** + * Create a live child session from a turn-enclosed prefix of a live source. + * `boundary` is an inclusive source event seq; omitted means the source's + * current last event. A non-empty selected slice must end at `turn/end`. + * + * @param source - Live source session object or id. + * @param boundary - Inclusive source event seq to fork through; omitted means + * the source's current last event, and omitted on an empty source forks an + * empty child. + * @param childSessionId - Optional child session id; omitted delegates to + * `SessionStore`'s id policy. + * @returns The created live child session. + */ fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session ``` @@ -245,9 +805,42 @@ Source: [`packages/core/session/src/index.ts:577`](../../packages/core/session/s Registry of skill providers. It merges provider catalogs with stable first-wins duplicate handling, exposes sorted model-visible summaries, and loads full skill bodies on demand. ```ts cordis-catalog +/** + * Register a borrowed same-process provider synchronously during plugin apply. Duplicate and + * reserved names throw; remote initialization belongs in `list()`. Fiber disposal unregisters + * the provider and invalidates catalog caches. + * @param provider - the provider to register by `provider.name`. + * @returns the exact Cordis effect disposer that unregisters this provider; + * composite effects may yield it directly to preserve teardown ordering. + */ registerProvider(provider: SkillProvider): () => void + +/** + * Register a borrowed readonly runtime skill. Project entries outrank runtime entries, which + * outrank user entries. Same-name runtime entries are first-wins; a duplicate logs a warning and + * receives a no-op disposer so it cannot remove the winner. + * @param skill - the complete skill definition to expose for discovery. + * @returns the exact Cordis effect disposer, preserving composite teardown order and invalidating caches. + */ register(skill: SkillRegistration): () => void + +/** + * List model-invocable skill summaries for a workspace. Lookup options and + * provider candidates are readonly same-process values borrowed throughout + * discovery. + * @param options - lookup options; `cwd` selects project roots and `signal` cancels discovery. + * @returns sorted summaries, excluding skills disabled for model invocation. + */ async list(options: SkillLookupOptions = {}): Promise + +/** + * Load and validate the winning candidate, passing its opaque discovery locator back to the + * provider. Cancellation is rechecked after selection, including cache hits, and raced against + * loading so an uncooperative provider cannot hang the caller. + * @param name - kebab-case skill name. + * @param options - lookup options; `cwd` selects workspace-sensitive skills and `signal` cancels work. + * @returns the full skill, including body content, or `undefined`. + */ async get(name: string, options: SkillLookupOptions = {}): Promise ``` @@ -264,6 +857,11 @@ Semantics every implementation must honor: - `saveText` REJECTS on a real storage failure (permissions, ENOSPC, backend unavailable); the caller decides how to degrade (the spill policy treats a rejection as best-effort and keeps the inline result). ```ts cordis-catalog +/** + * Persist `input.content` to a session-scoped spill artifact. + * @param input - the owner, provenance, suggested name, and full text to save. + * @returns the saved artifact's {@link SpillRef}; rejects on a storage failure. + */ abstract saveText(input: SaveTextSpill): Promise ``` @@ -274,9 +872,37 @@ Source: [`packages/spill/spill/src/index.ts:45`](../../packages/spill/spill/src/ Named provider registry and capability-checked start surface. ```ts cordis-catalog +/** + * Register a provider under its name. Registration is effect-scoped and HMR + * safe; removing a provider blocks new starts but does not revoke runs that + * were already returned to their holders. + * @param provider - the trusted provider implementation. + * @returns the exact Cordis effect disposer. + */ registerProvider(provider: SubagentProvider): () => void + +/** + * Look up a provider by name. + * @param name - the provider name. + * @returns the provider, or undefined when absent. + */ getProvider(name: string): SubagentProvider | undefined + +/** + * List registered provider names in insertion order. + * @returns the registered names. + */ list(): string[] + +/** + * Establish a ready child on the named provider. Capability and semantic + * checks run before delegation. Provider ownership lasts until its promise + * fulfills; a rejection therefore has no run for the caller to dispose and + * emits no run lifecycle events. + * @param name - the provider to use. + * @param request - child prompt, parent, signal, and optional capabilities. + * @returns the ready holder-owned run. + */ async start(name: string, request: SubagentStartRequest): Promise ``` @@ -287,9 +913,42 @@ Source: [`packages/subagent/subagent/src/index.ts:153`](../../packages/subagent/ Registry service for the prompt inputs assembled before each model step. ```ts cordis-catalog +/** + * Register an ordered prompt section in the calling context's scope. A scoped + * section shadows a global section with the same name; duplicates within one + * layer and non-finite orders throw. Registration and disposal emit + * `system-prompt/change`. + * @param section - the section to register. + * @returns the exact Cordis effect disposer. + */ section(section: PromptSection): () => void + +/** + * Register a tool-schema provider in the calling context's scope. Global and + * matching scoped providers both contribute; returning the reserved + * {@link TOOL_ORDER_REST} name makes assembly fail. + * @param provider - evaluated for each assembly with its context. + * @returns the exact Cordis effect disposer. + */ tools(provider: (context: AssembleContext) => ToolProviderResult): () => void + +/** + * Register a prompt variable in the calling context's scope. Scoped values + * shadow globals; invalid or duplicate names throw. A provider may return + * `undefined`, but rendering a section that references that value then fails. + * @param name - the `[a-z][a-z0-9_]*` reference name. + * @param provider - evaluated for each assembly. + * @returns the exact Cordis effect disposer. + */ variable(name: string, provider: (context: AssembleContext) => string | undefined): () => void + +/** + * Assemble global and scoped providers, detach tool parameters, apply + * canonical ordering, then run the assembly waterfall. Scoped sections and + * variables shadow globals; the returned waterfall value is authoritative. + * @param context - the optional scope and plugin-defined assembly fields. + * @returns the authoritative post-waterfall assembly. + */ async assemble(context: AssembleContext = {}): Promise ``` @@ -300,13 +959,83 @@ Source: [`packages/core/system-prompt/src/index.ts:209`](../../packages/core/sys The `tasks` service: the runtime-global background task registry. See the module doc for the ownership, isolation, and lifecycle contracts. ```ts cordis-catalog +/** + * Preflight access, validation, and owner cleanup before starting and + * atomically registering work. A throwing starter leaves nothing registered; + * after it returns, registration cannot fail. Settlement records the outcome, + * notifies listeners, and releases waiters. + * @param spec - task identity, owner, and synchronous starter. + * @returns the registry-issued `-N` id. + */ start(spec: TaskStart): TaskId + +/** + * List caller-owned and unowned tasks in registration order without exposing + * another session's labels. + * @param caller - reading agent; a non-agent caller sees only unowned tasks. + * @returns fresh snapshots. + */ list(caller?: Agent): TaskSnapshot[] + +/** + * Return a non-consuming snapshot without changing its read cursor or notice + * state. Throws for an unknown or foreign task. + * @param id - task to look up. + * @param caller - reading agent checked against the owner. + * @returns a fresh snapshot. + */ get(id: TaskId, caller?: Agent): TaskSnapshot + +/** + * Read the next stream delta, or the idempotent final output after settlement. + * A terminal read marks the task reported. Throws for an unknown or foreign + * task. + * @param id - task to read. + * @param caller - reading agent checked against the owner. + * @returns output text and the post-read snapshot. + */ read(id: TaskId, caller?: Agent): TaskRead + +/** + * Request cancellation, then mark the task stopping and reported. A producer + * throw propagates without changing task state. Throws for an unknown or + * foreign task. + * @param id - task to cancel. + * @param caller - killing agent checked against the owner. + * @param reason - logged reason forwarded to the producer. + * @returns `requested` for live work, otherwise `already-finished`. + */ kill(id: TaskId, caller?: Agent, reason?: string): 'requested' | 'already-finished' + +/** + * Wait for settlement or timeout without cancelling the task. Caller abort + * rejects only while the task is live; after settlement it returns the + * terminal snapshot so a notice suppressed for this waiter is still delivered. + * Timed-out and aborted waits detach their resolvers. Throws for invalid, + * unknown, or foreign input. + * @param id - task to wait for. + * @param timeoutMs - positive finite wait bound in milliseconds. + * @param caller - waiting agent checked against the owner. + * @param signal - optional cancellation of the wait itself. + * @returns snapshot at settlement or timeout. + */ async wait(id: TaskId, timeoutMs: number, caller?: Agent, signal?: AbortSignal): Promise + +/** + * Register an effect-scoped completion listener. Each listener is contained; + * returned promises are observed but not awaited. No listener runs after + * service disposal. + * @param listener - receives each terminal snapshot and its exact owner. + * @returns disposer that unregisters the listener. + */ onTaskDone(listener: TaskDoneListener): () => void + +/** + * Attach an effect-scoped surface that can read and stop tasks. {@link start} + * refuses work while none is attached. + * @param name - diagnostic label; duplicate names remain independent. + * @returns disposer that detaches this surface. + */ attachSurface(name: string): () => void ``` @@ -319,7 +1048,29 @@ Source: [`packages/tasks/tasks/src/index.ts:76`](../../packages/tasks/tasks/src/ Replay owner for one service-wide estimator and isolated per-session folds. ```ts cordis-catalog +/** + * Measure current request pressure and surface through the durable tail. + * + * Provider usage is reused only when the latest successful call's canonical + * request envelope matches `requestHeader` and its total is no lower than + * that call's full heuristic anchor; otherwise the complete envelope and + * surface are heuristically repriced. + * + * `requestHeader` affects request pressure only; surface fields always + * describe the current session surface. Every call clones those positional + * nodes, so measurement is O(surface). + * + * @param session - session to replay through its current durable tail. + * @param requestHeader - optional effective request envelope replacing the latest logged header. + * @returns a detached deeply immutable pressure and surface measurement. + */ measure(session: Session, requestHeader?: EpochHeader): TokenMeasurement + +/** + * Heuristically price one model-visible message. + * @param message - message to price without mutation. + * @returns content and role-framing tokens under the fixed service heuristic. + */ estimateMessage(message: Message): number ``` @@ -332,12 +1083,72 @@ Source: [`packages/llm/token-meter/src/index.ts:106`](../../packages/llm/token-m Tool registry and execution pipeline. Scoped registrations shadow globals; one visibility resolver feeds presentation, lookup, and dispatch. ```ts cordis-catalog +/** + * Register globally or in the calling agent scope. Scoped tools shadow + * globals; duplicates within one layer and the reserved `run_code` name fail. + * @param definition - the tool schema, execution, and optional presentation functions. + * @returns the exact disposer that unregisters the tool. + */ register(definition: ToolDefinition): () => void + +/** + * Restrict global tools for the calling agent scope. Empty filters, unknown + * names, scope-local names, and reserved transport names fail. Restrictions + * intersect; scoped registrations remain visible. + * @param filter - global-surface mask: `allow` (keep only) and/or `deny` (remove). + * @returns the exact disposer that lifts this restriction. + */ restrict(filter: ToolRestriction): () => void + +/** + * Register a monotonic guard after the extensible `tools/pre-execute` + * waterfall. A plain-context guard applies globally; one registered through + * `agent.ctx` applies only to that agent. Any matching guard may deny by + * returning a reason, while no guard can force-allow a call another guard + * denied. The exact effect disposer is returned for ordered ownership and + * HMR cleanup. + * @param guard - synchronous check; a returned string denies the execution. + * @returns the exact disposer that unregisters the guard. + */ guard(guard: ToolGuard): () => void + +/** + * Look up a tool as one scope sees it (scoped + * shadows global; a restricted-away global reads as absent). Presenters pass + * the calling agent so the rendered card matches the definition that + * actually executed. + * @param name - the tool name as registered. + * @param scope - the viewing scope (the agent); omitted = the global view. + * @returns the definition the scope resolves, or undefined when none is visible. + */ get(name: string, scope?: ScopeKey): ToolDefinition | undefined + +/** + * Project visible definitions onto the allowlisted model-facing schema fields, + * excluding execution and presentation callbacks. + * @param scope - the viewing scope (the agent); omitted = the global view. + * @returns one deep-cloned schema per visible tool. + */ schemas(scope?: ScopeKey): ToolSchema[] + +/** + * Classify a pending call through the caller's visible tool definition. Only + * an exact `true` is parallel; unknown, hidden, undeclared, invalid, or + * throwing classifiers are exclusive. + * @param exec - call name, parsed arguments, and optional agent scope. + * @returns the fail-closed scheduling mode. + */ executionMode(exec: ToolExecutionInput): ToolExecutionMode + +/** + * Execute through pre-policy, guards, around-dispatch, post-policy, and final + * notification. Tool and listener failures resolve as materialized error + * results; an invisible tool reports `UNKNOWN_TOOL`. The returned outcome is + * the same lossless, frozen snapshot final observers receive. + * @param exec - the typed same-process call input. The registry assigns its + * correlation token before policy begins. + * @returns the materialized final result. + */ async execute(exec: ToolExecutionInput): Promise ``` @@ -350,7 +1161,20 @@ Source: [`packages/core/tools/src/index.ts:438`](../../packages/core/tools/src/i `ctx.userInteraction`: one active UI provider plus an `ask()` surface. ```ts cordis-catalog +/** + * Register the UI provider. Only one provider may be active in a context. + * + * @param provider UI-side implementation that collects answers. + * @returns Disposer that unregisters this provider. + */ registerProvider(provider: UserInteractionProvider): () => void + +/** + * Ask the active UI provider and wait for the user's answer. + * + * @param request Questions, owner agent, and abort signal. + * @returns The answer chosen or typed by the human. + */ async ask(request: AskUserQuestionRequest): Promise ``` @@ -370,9 +1194,43 @@ Selection semantics (resolved at execution time, never order-dependent): - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`. ```ts cordis-catalog +/** + * Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` + * if its id is already registered for search. Returns a disposer; disposed + * with the calling fiber. + * @param provider - the provider; its `id` is the registry key. + * @returns the disposer that unregisters the provider. + */ registerSearchProvider(provider: WebSearchProvider): () => void + +/** + * Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER` + * if its id is already registered for fetch. Returns a disposer; disposed + * with the calling fiber. + * @param provider - the provider; its `id` is the registry key. + * @returns the disposer that unregisters the provider. + */ registerFetchProvider(provider: WebFetchProvider): () => void + +/** + * Run one search through the selected provider. Resolves the provider at call + * time with the selection rules above; throws {@link WebError} when the + * capability cannot run. The seam enforces `request.maxResults` on the result: + * if the provider over-returns, `sources[]` is truncated and `truncated` set. + * @param request - the query plus result-shaping options. + * @param signal - optional cancellation signal forwarded to the provider. + * @returns the provider's results, capped to `request.maxResults`. + */ async search(request: WebSearchRequest, signal?: AbortSignal): Promise + +/** + * Retrieve one URL through the selected provider. Resolves the provider at + * call time with the selection rules above; throws {@link WebError} when the + * capability cannot run. A non-2xx response is a result, not a throw. + * @param request - the URL plus retrieval options. + * @param signal - optional cancellation signal forwarded to the provider. + * @returns the retrieval outcome; non-2xx responses resolve descriptively. + */ async fetch(request: WebFetchRequest, signal?: AbortSignal): Promise ``` @@ -383,6 +1241,12 @@ Source: [`packages/web/web/src/index.ts:74`](../../packages/web/web/src/index.ts Workflow execution seam. Invalid requests throw before publication; a live run is holder-owned, its result never rejects, cancellation and disposal are bounded, and disposal waits for child cleanup within that bound. Lifecycle listener failures are contained, and `workflow/end` fires exactly once as the result settles. ```ts cordis-catalog +/** + * Parse and execute a workflow script. + * @param request - the script, its `args`, the parent agent, and an + * optional cancel signal. + * @returns the live run; its `result` resolves when the script settles. + */ abstract start(request: WorkflowStartRequest): WorkflowRun ``` diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 9efe04e63c..ac2440cd7c 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -403,49 +403,9 @@ interface Agent { The [event taxonomy](../architecture.md#event) owns the `agent/*` lifecycle, checkpoint, and waterfall contracts. Turn and step boundaries are durable session events rather than agent emits. -## Agent execution context +## Initiating Agent -`AgentExecution` is the process-local ambient frame established around a concrete driver's lifetime. It holds the exact Agent object rather than duplicating Session or step state; ambient presence is neither liveness proof nor authorization. - -Source: [`packages/core/agent-execution/src/types.ts`](../../packages/core/agent-execution/src/types.ts) - -```ts type-equiv -interface AgentExecution { - readonly agent: Agent -} -``` - -The mandatory service reads, requires, establishes, or explicitly clears that frame. `run()` preserves the operation's exact synchronous value or Promise. - -Source: [`packages/core/agent-execution/src/index.ts`](../../packages/core/agent-execution/src/index.ts) - -```ts type-equiv -interface AgentExecutionService { - /** - * Read the active execution without requiring one. - * @returns the inherited execution, or `undefined` outside/inside a cleared boundary. - * @throws when this service instance has been disposed. - */ - current(): AgentExecution | undefined - - /** - * Read the active execution and fail when no boundary is active. - * @returns the inherited execution. - * @throws when no execution is active or this service instance has been disposed. - */ - require(): AgentExecution - - /** - * Run an operation inside an execution boundary. Passing `undefined` clears - * an inherited execution; the exact synchronous value or Promise is returned. - * @param execution - execution to inherit, or `undefined` for a clearing boundary. - * @param operation - synchronous or asynchronous operation to invoke. - * @returns the exact value returned by `operation`. - * @throws when this service is closing/disposed, or when `operation` throws. - */ - run(execution: AgentExecution | undefined, operation: () => T): T -} -``` +The process-local initiator carried by `ctx.agents` is the exact `Agent` above, not a separate frame or copied identity. Ambient presence is neither liveness proof nor authorization; the [initiator-scope decision](../rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns its lifetime and boundary rules. ## Interception decisions diff --git a/docs/module-graph.md b/docs/module-graph.md index c65b31a6fc..edb6034216 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -22,7 +22,6 @@ flowchart TD end subgraph group_core["packages/core"] pkg_agent["agent"] - pkg_agent_execution["agent-execution"] pkg_agent_loop["agent-loop"] pkg_scope["scope"] pkg_session["session"] @@ -194,7 +193,6 @@ flowchart TD pkg_llm_replay --> pkg_session pkg_sandbox_local --> pkg_llm pkg_sandbox_local --> pkg_sandbox - pkg_agent_execution --> pkg_agent pkg_bash_local --> pkg_bash pkg_bash_local --> pkg_timeout pkg_compact_basic --> pkg_agent @@ -248,7 +246,6 @@ flowchart TD pkg_permission --> pkg_session pkg_permission --> pkg_user_approval pkg_agent_loop --> pkg_agent - pkg_agent_loop --> pkg_agent_execution pkg_agent_loop --> pkg_llm pkg_agent_loop --> pkg_scope pkg_agent_loop --> pkg_session @@ -311,7 +308,6 @@ flowchart TD pkg_hooks_codex --> pkg_session_persistence pkg_hooks_codex --> pkg_tools pkg_agent_loop_testkit --> pkg_agent - pkg_agent_loop_testkit --> pkg_agent_execution pkg_agent_loop_testkit --> pkg_llm pkg_agent_loop_testkit --> pkg_session pkg_agent_loop_testkit --> pkg_system_prompt @@ -390,7 +386,6 @@ flowchart TD pkg_tui --> pkg_tools pkg_tui --> pkg_user_interaction pkg_agent_spine_demo --> pkg_agent - pkg_agent_spine_demo --> pkg_agent_execution pkg_agent_spine_demo --> pkg_agent_loop pkg_agent_spine_demo --> pkg_home pkg_agent_spine_demo --> pkg_invariants @@ -481,7 +476,6 @@ flowchart TD | [`session-persistence`](../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../packages/core/session) | | [`llm-replay`](../packages/support/llm-replay) | `support` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`sandbox-local`](../packages/sandbox/sandbox-local) | `sandbox` | [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox) | -| [`agent-execution`](../packages/core/agent-execution) | `core` | [`agent`](../packages/core/agent) | | [`bash-local`](../packages/bash/bash-local) | `bash` | [`bash`](../packages/bash/bash), [`timeout`](../packages/util/timeout) | | [`compact-basic`](../packages/compact/compact-basic) | `compact` | [`agent`](../packages/core/agent), [`compact`](../packages/compact/compact), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`token-meter`](../packages/llm/token-meter) | | [`spill-local`](../packages/spill/spill-local) | `spill` | [`spill`](../packages/spill/spill) | @@ -498,7 +492,7 @@ flowchart TD | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) | | [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) | | [`permission`](../packages/ui/permission) | `ui` | [`bash`](../packages/bash/bash), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`user-approval`](../packages/ui/user-approval) | -| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`agent-execution`](../packages/core/agent-execution), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`home`](../packages/util/home), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-fs-search`](../packages/fs/tool-fs-search) | `fs` | [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`retention`](../packages/util/retention), [`session`](../packages/core/session), [`spill`](../packages/spill/spill), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | @@ -510,7 +504,7 @@ flowchart TD | [`tool-todo`](../packages/todo/tool-todo) | `todo` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | | [`tool-cordis`](../packages/cordis/tool-cordis) | `cordis` | [`scope`](../packages/core/scope), [`tools`](../packages/core/tools) | | [`hooks-codex`](../packages/hooks/hooks-codex) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`tools`](../packages/core/tools) | -| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`agent-execution`](../packages/core/agent-execution), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | +| [`agent-loop-testkit`](../packages/support/agent-loop-testkit) | `support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`acp`](../packages/ui/acp) | `ui` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`permission`](../packages/ui/permission), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval), [`user-interaction`](../packages/ui/user-interaction) | | [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | | [`workspace-context`](../packages/context/workspace-context) | `context` | [`agent`](../packages/core/agent), [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`paths`](../packages/util/paths), [`session`](../packages/core/session), [`tools`](../packages/core/tools) | diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index 73d395af4b..16a5563532 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -163,7 +163,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [Single-file executable SDK runtime distribution (single-exe)](implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md) | 2026-07-10 | | [Agent-scope runtime design and correctness](implemented/architecture/2026-07-12-agent-scope-runtime-design.md) | 2026-07-12 | | [Provider-routed LLM adapters and a generic pi-ai backend](implemented/architecture/2026-07-14-provider-routed-llm-adapters.md) | 2026-07-14 | -| [Agent execution context over AsyncLocalStorage](implemented/architecture/2026-07-15-agent-execution-context.md) | 2026-07-15 | +| [Initiating Agent scope over AsyncLocalStorage](implemented/architecture/2026-07-15-agent-initiator-scope.md) | 2026-07-15 | | [Advisory LLM catalogs and per-session ACP model selection](implemented/architecture/2026-07-15-llm-model-catalog-and-acp-selection.md) | 2026-07-15 | | [Replay token meter service](implemented/architecture/2026-07-15-replay-token-meter-service.md) | 2026-07-15 | diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.md b/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.md deleted file mode 100644 index 2ea88cddf7..0000000000 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.md +++ /dev/null @@ -1,61 +0,0 @@ -# RFC: Agent execution context over AsyncLocalStorage - -Status: implemented - -English | [中文](2026-07-15-agent-execution-context.zh.md) - -## Problem - -The harness has two useful but different notions of context. A Cordis `Context` selects services, registration ownership, and lifetime; `agent.ctx` is the flat registration scope owned by one live Agent. Agent and Session identity instead describe the subject of an asynchronous operation. Changing a root `ctx.agent` to mean “whichever Agent is running” would conflate those meanings and fail when one process drives Agents concurrently. - -Deep process-local infrastructure sometimes needs a trusted initiating Agent below explicit loop, tool, and request parameters—for example, a host-aware transport, tracing helper, logger, or gateway client. Requiring every private helper to forward `agent` adds repetition, while a process-global mutable slot is incorrect across `await`. Model-visible arguments are unsuitable because a model must not choose a trusted Session or routing header. The carrier is mandatory control infrastructure rather than optional model-visible context. - -## Decision - -`@deepseek-ai/dsh-agent-execution` provides the mandatory `ctx.agentExecution` service using Node `AsyncLocalStorage`. The named `AgentExecution` frame contains only the exact Agent object; the [core-data catalog](../../../core-data-structures/core.md#agent-execution-context) owns the literal frame and service types. - -`current()` reads optionally, `require()` throws `no agent execution context is active`, and `run()` preserves the operation's exact synchronous value or Promise. `run(undefined, operation)` establishes a clearing boundary for work that must not inherit an Agent. Session remains derived as `execution.agent.session`; turn, step, tool call, `signal`, model, `cwd`, sandbox, and authorization stay with their existing owners. - -`AgentLoop` injects the service and wraps each concrete driver's complete `runLoop` lifetime in `agentExecution.run({ agent }, ...)`. Concurrent drivers therefore receive independent stores, a child driver shadows its parent, and the parent store returns when the child boundary settles. Creation, persistence load, and unpublished `setup(agentCtx)` remain outside the child's driver boundary: creation initiated by a parent runs under the parent identity, while `agentCtx.agent` explicitly identifies the child. - -Ambient identity does not replace explicit contracts. `ToolExecution.agent`, `AssembleContext.agent`, `GenerateOptions.sessionId`, task ownership, parent/child requests, `ctx.agent`, `agentCtx.agent`, approval and hook subjects, `cwd` selection, cancellation, worker/process messages, persistence records, and wire identity remain explicit. A remote boundary materializes the identity it needs into its typed request because ALS is process-local. - -The provider uses an ordered composite effect. Teardown first rejects new boundaries, then removes the service and awaits injected dependents such as AgentLoop, then waits for active returned-Promise boundaries before calling `AsyncLocalStorage.disable()`. `current()` and `require()` remain usable through a retained in-flight service reference while that drain runs; after disposal, retained calls throw `agent execution service is disposed`. Root Context disposal may start sibling fiber teardown concurrently, so active-boundary counting is required in addition to Cordis dependency ordering. - -`run()` does not own detached work: provider drain tracks only the Promise returned by `operation`. Asynchronous resources created inside the boundary inherit its store until they settle or ALS is disabled, so their owning seam must stop unreturned work explicitly. Agent-owned foreground work returns its lifetime and keeps its cancellation contract. Unrelated timers, queues, and deployment infrastructure start under `run(undefined, operation)`; queue, worker, process, and wire boundaries serialize identity rather than expecting ALS propagation. - -A host-aware transport may derive a deployment-owned header such as `X-Harness-Session-Id` from `ctx.agentExecution.require().agent.session.id`; the header is absent from model-visible schema and arguments. No production MCP or Web transport adopts such a header in this decision. A test-double transport proves the trusted boundary without assigning host routing policy to an existing provider-neutral seam. - -This decision extends the [Agent registration-scope contract](2026-07-08-agent-scope-contexts.md) and its [runtime design](2026-07-12-agent-scope-runtime-design.md); it does not change their static `agent.ctx` meaning. - -## Verification - -Service tests pin optional and required reads, exact synchronous and cross-realm Promise identity, overlapping, nested, and cleared boundaries, restoration after throws or rejection, drain ordering, and retained-reference errors. AgentLoop integration pins concurrent and nested drivers, agentless calls, missing-provider activation, service restart, and provider/root teardown. Composition, module-graph, build, and runtime-closure checks keep the provider wired through the default bundle, SDK spine, Python runtime closure, and direct AgentLoop harnesses. - -Only a test-double host-aware transport consumes ambient identity; it derives `X-Harness-Session-Id` internally and verifies that tool schema and logged arguments contain no identity field. The service deliberately does not drain async work omitted from the Promise returned by `operation`; that work remains subject to its owner's explicit stop contract. - -## Alternatives considered - -**Pass Agent through every function.** Public, worker, process, persistence, and wire boundaries continue to do this, but requiring every process-local private helper to carry Agent adds repetitive forwarding without improving trust. ALS is confined to the asynchronous chain inside those explicit boundaries. - -**Make `ctx.agent` dynamic.** `ctx.agent` already means the static Agent associated with an Agent-scoped Cordis context. Changing the root meaning would mix registration and execution scopes and make concurrent behavior surprising. - -**Store a complete mutable runtime frame.** Agent, Session, inbox, cancellation, turn, step, tool execution, and persistence already have authoritative owners. Duplicating them would create stale snapshots and another lifecycle. A named frame makes the execution-context boundary explicit without duplicating owner state. - -**Include a step `AbortSignal`, `cwd`, sandbox, or authorization.** Their lifetimes and authority do not match the driver boundary, and their existing seams already pass them explicitly. Adding a control capability requires a separate decision and nested lifecycle contract. - -**Use a process-global `currentAgent`.** Concurrent Agents and subagents overwrite one another across awaited continuations, so a mutable global is correct only under a serialization guarantee the harness does not make. - -**Derive identity from model-visible arguments.** Model or user input cannot be trusted to select Session, tenant, or sandbox routing. - -**Add routing identity to every capability seam.** That spreads hosting concerns through provider-neutral APIs. A host-aware implementation owns its transport header while public boundaries remain explicit. - -## Consequences - -Deep infrastructure gains one trusted process-local initiating Agent without widening existing tool and capability requests. Concurrent and nested drivers isolate automatically, AgentLoop stays inactive when the provider is absent, and HMR/root disposal reaches quiescence before ALS is disabled. - -The dependency is implicit in function signatures and carries a capability-bearing Agent object. Consumers must restrict it to cross-cutting infrastructure, treat ambient presence as neither liveness nor authorization, and retain explicit cancellation and ownership checks. ALS also has an always-on propagation cost and does not cross worker, process, HTTP, or durable queue boundaries. - -The teardown design deliberately accepts Node's [Stability 1 (Experimental)](https://nodejs.org/api/async_context.html#asynclocalstoragedisable) `AsyncLocalStorage.disable()` dependency. Node requires `disable()` before an ALS instance can be garbage-collected, which matters when HMR replaces provider-owned instances; the service state guard prevents a later `run()` from re-entering the instance after disposal. - -The frame deliberately omits turn, step, `signal`, `cwd`, sandbox, and authorization. A real consumer that cannot use existing explicit fields must justify any refinement separately; a stale copied field may at most mislabel telemetry, never grant control. diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.zh.md b/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.zh.md deleted file mode 100644 index 1c4e0ba9c3..0000000000 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.zh.md +++ /dev/null @@ -1,61 +0,0 @@ -# RFC: 基于 AsyncLocalStorage 的 Agent 执行上下文 - -Status: implemented - -[English](2026-07-15-agent-execution-context.md) | 中文 - -## 问题 - -Harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负责选择服务、注册归属和生命周期;`agent.ctx` 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若把根 `ctx.agent` 改成「当前正在运行的 Agent」,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。 - -进程内深层基础设施有时需要在显式传递的循环、工具及请求参数之下获取可信的发起 Agent,例如宿主感知传输层、追踪辅助函数、日志器或网关客户端。要求每个私有辅助函数都转发 `agent` 会造成重复,而进程级可变槽会在跨 `await` 时发生并发错误。模型可见参数也不适用,因为模型不得选择可信的会话或路由请求头。该载体属于必需的控制基础设施,而非模型可见的可选上下文。 - -## 决策 - -`@deepseek-ai/dsh-agent-execution` 使用 Node `AsyncLocalStorage` 提供必需的 `ctx.agentExecution` 服务。命名的 `AgentExecution` 帧仅包含同一个 Agent 对象;[核心数据目录](../../../core-data-structures/core.md#agent-execution-context)是帧与服务字面类型定义的真源。 - -`current()` 用于可选读取,`require()` 抛出 `no agent execution context is active`,`run()` 保留操作返回的同步值或 Promise 本身。`run(undefined, operation)` 会建立清空边界,供不得继承 Agent 的工作使用。会话仍通过 `execution.agent.session` 推导;轮次、步骤、工具调用、`signal`、模型、`cwd`、沙箱和授权继续由现有归属方管理。 - -`AgentLoop` 注入该服务,并用 `agentExecution.run({ agent }, ...)` 包裹每个具体驱动的完整 `runLoop` 生命周期。因此,并发驱动使用彼此独立的存储,子驱动会遮蔽父驱动,子边界结束后父存储得到恢复。创建、持久化加载和尚未发布的 `setup(agentCtx)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 `agentCtx.agent` 显式标识子 Agent。 - -隐式身份不会取代显式契约。`ToolExecution.agent`、`AssembleContext.agent`、`GenerateOptions.sessionId`、任务归属、父子请求、`ctx.agent`、`agentCtx.agent`、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。 - -提供方使用有序复合 effect。teardown 会先拒绝新边界,再移除服务并等待 AgentLoop 等注入方排空,随后等待活动的返回 Promise 边界,最后调用 `AsyncLocalStorage.disable()`。排空期间,进行中代码可通过保留的服务引用继续调用 `current()` 和 `require()`;dispose 后,保留引用会抛出 `agent execution service is disposed`。根 Context dispose 可能并发启动同级 fiber 的 teardown,因此除 Cordis 依赖顺序外还必须统计活动边界。 - -`run()` 不负责管理脱离返回链的工作:提供方排空只跟踪 `operation` 返回的 Promise。边界内创建的异步资源会继承其存储,直到自身结束或 ALS 被禁用;所属 seam 必须显式停止未纳入返回 Promise 的工作。Agent 所有前台工作会把完整生命周期纳入返回值,并保留显式取消契约。无关的定时器、队列和部署基础设施在 `run(undefined, operation)` 下启动;队列、worker、进程和协议边界必须序列化身份,不能期待 ALS 传播。 - -宿主感知的传输层可以从 `ctx.agentExecution.require().agent.session.id` 推导由部署方拥有的 `X-Harness-Session-Id` 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。 - -本决策扩展 [Agent 注册作用域契约](2026-07-08-agent-scope-contexts.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.md),不会改变其中 `agent.ctx` 的静态含义。 - -## 验证 - -服务测试锁定可选与必需读取、同步值和跨 realm Promise 的引用身份、并发、嵌套及清空边界、同步抛错或 Promise 拒绝后的恢复、排空顺序及保留引用的错误。AgentLoop 集成测试锁定并发与嵌套驱动、无 Agent 调用、缺少提供方时的激活行为、服务重启,以及提供方或根 Context 的销毁流程。组合、模块图、构建及运行时闭包检查确保默认组合包、SDK 主干、Python 运行时闭包及直接 AgentLoop harness 都装载提供方。 - -只有测试替身形式的宿主感知传输层消费隐式身份;它在内部推导 `X-Harness-Session-Id`,并验证工具 schema 与记录参数都不包含身份字段。服务有意不排空 `operation` 返回的 Promise 之外的异步工作;这类工作仍由所属方的显式停止契约管理。 - -## 考虑过的替代方案 - -**在每个函数中传递 Agent。** 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会造成重复转发,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。 - -**让 `ctx.agent` 变成动态值。** `ctx.agent` 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。 - -**保存完整的可变运行时帧。** Agent、会话、inbox、取消、轮次、步骤、工具执行和持久化已经有各自的真源。重复保存会产生陈旧快照和另一套生命周期。命名帧能够明确标识执行上下文边界,而不重复保存归属方状态。 - -**包含步骤级 `AbortSignal`、`cwd`、沙箱或授权。** 它们的生命周期及权限范围与驱动边界不一致,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期契约。 - -**使用进程级 `currentAgent`。** 并发 Agent 和 subagent 会在异步延续执行之间相互覆盖,因此可变全局值只在 Harness 不具备的串行保证下才正确。 - -**从模型可见参数推导身份。** 不能信任模型或用户输入来选择会话、租户或沙箱路由。 - -**给每个能力 seam 增加路由身份。** 这会把宿主关注点扩散到提供方无关 API。宿主感知实现拥有其传输请求头,而公开边界继续显式传递身份。 - -## 后果 - -深层基础设施可以获得一个可信的进程内发起 Agent,而无需加宽现有工具和能力请求。并发及嵌套驱动会自动隔离,缺少提供方时 AgentLoop 保持未激活,HMR 或根 Context dispose 会在禁用 ALS 前完成排空。 - -该依赖不会出现在函数签名中,并且携带一个具有控制能力的 Agent 对象。消费方必须将其限制在横切基础设施中,把隐式存在视为既不证明存活、也不授予权限,并保留显式取消和归属检查。ALS 还有常驻传播成本,也无法跨越 worker、进程、HTTP 或持久化队列边界。 - -该销毁设计有意依赖 Node 的 [Stability 1(实验性)](https://nodejs.org/api/async_context.html#asynclocalstoragedisable) API `AsyncLocalStorage.disable()`。Node 要求在 ALS 实例可被垃圾回收前调用 `disable()`,这对 HMR 替换提供方所拥有的实例尤为重要;服务状态守卫会阻止 dispose 后通过后续 `run()` 重新进入该实例。 - -该帧有意省略轮次、步骤、`signal`、`cwd`、沙箱和授权。若真实消费方无法使用现有显式字段,必须另行论证扩展;陈旧字段最多只能误标遥测数据,绝不能授予控制权。 diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml similarity index 64% rename from docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.i18n.yaml rename to docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml index 371884096a..9d9e11522f 100644 --- a/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write -2026-07-15-agent-execution-context.md: 2ea88cddf7a686713157c77ce1d7520082a9da28 -2026-07-15-agent-execution-context.zh.md: 1c4e0ba9c3fa787dfa816eff4c5d04cff5c2f7b1 +2026-07-15-agent-initiator-scope.md: 480bb56f811567406e964ca94fca04fd3d4ace62 +2026-07-15-agent-initiator-scope.zh.md: 29ea3a47f42d607a4584b888ace232de124e7598 diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md new file mode 100644 index 0000000000..480bb56f81 --- /dev/null +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md @@ -0,0 +1,63 @@ +# RFC: Initiating Agent scope over AsyncLocalStorage + +Status: implemented + +English | [中文](2026-07-15-agent-initiator-scope.zh.md) + +## Problem + +The harness has two useful but different notions of context. A Cordis `Context` selects services, registration ownership, and lifetime; `agent.ctx` is the flat registration scope owned by one live Agent. Agent and Session identity instead describe the subject of an asynchronous operation. Changing a root `ctx.agent` to mean “whichever Agent is running” would conflate those meanings and fail when one process drives Agents concurrently. + +Deep process-local infrastructure sometimes needs a trusted initiating Agent below explicit loop, tool, and request parameters—for example, a host-aware transport, tracing helper, logger, or gateway client. Requiring every private helper to forward `agent` adds repetition, while a process-global mutable slot is incorrect across `await`. Model-visible arguments are unsuitable because a model must not choose a trusted Session or routing header. The carrier belongs to the Agent service rather than optional model-visible context. + +## Decision + +The mandatory `ctx.agents` service uses Node `AsyncLocalStorage` to carry the initiating Agent. It stores the exact `Agent` directly rather than introducing a one-field frame; the [core-data catalog](../../../core-data-structures/core.md#initiating-agent) identifies the carried type. + +`currentInitiator()` reads optionally, `requireInitiator()` throws `no initiating agent is active`, and `withInitiator(agent, operation)` preserves the operation's exact synchronous value or Promise. `withoutInitiator(operation)` establishes a clearing boundary for work that must not inherit an Agent. Session remains derived as `agent.session`; turn, step, tool call, `signal`, model, `cwd`, sandbox, and authorization stay with their existing owners. + +`AgentLoop` already injects `ctx.agents` and wraps each concrete driver's complete `runLoop` lifetime in `agents.withInitiator(agent, ...)`. Concurrent drivers therefore receive independent stores, a child driver shadows its parent, and the parent store returns when the child boundary settles. Creation, persistence load, and unpublished `setup(agentCtx)` remain outside the child's driver boundary: creation initiated by a parent runs under the parent identity, while `agentCtx.agent` explicitly identifies the child. + +Ambient identity does not replace explicit contracts. `ToolExecution.agent`, `AssembleContext.agent`, `GenerateOptions.sessionId`, task ownership, parent/child requests, `ctx.agent`, `agentCtx.agent`, approval and hook subjects, `cwd` selection, cancellation, worker/process messages, persistence records, and wire identity remain explicit. A remote boundary materializes the identity it needs into its typed request because ALS is process-local. + +`AgentRegistry` owns an ordered initiator lifecycle. Teardown first rejects new boundaries; removing `ctx.agents` then drains injected dependents such as AgentLoop, and the registry waits for active returned-Promise boundaries before calling `AsyncLocalStorage.disable()`. `currentInitiator()` and `requireInitiator()` remain usable through a retained in-flight service reference while that drain runs; after disposal, initiator methods throw `agent initiator scope is disposed`. Root Context disposal may start sibling fiber teardown concurrently, so active-boundary counting remains necessary in addition to Cordis dependency ordering. + +Initiator scope does not own detached work: registry drain tracks only the Promise returned by `withInitiator()` or `withoutInitiator()`. Asynchronous resources created inside a boundary inherit its store until they settle or ALS is disabled, so their owning seam must stop unreturned work explicitly. Agent-owned foreground work returns its lifetime and keeps its cancellation contract. Unrelated timers, queues, and deployment infrastructure start under `withoutInitiator(operation)`; queue, worker, process, and wire boundaries serialize identity rather than expecting ALS propagation. + +A host-aware transport may derive a deployment-owned header such as `X-Harness-Session-Id` from `ctx.agents.requireInitiator().session.id`; the header is absent from model-visible schema and arguments. No production MCP or Web transport adopts such a header in this decision. A test-double transport proves the trusted boundary without assigning host routing policy to an existing provider-neutral seam. + +This decision extends the [Agent registration-scope contract](2026-07-08-agent-scope-contexts.md) and its [runtime design](2026-07-12-agent-scope-runtime-design.md); it does not change their static `agent.ctx` meaning. + +## Verification + +Agent service tests pin optional and required reads, exact synchronous and cross-realm Promise identity, overlapping, nested, and cleared boundaries, restoration after throws or rejection, drain ordering, and retained-reference errors. AgentLoop integration pins concurrent and nested drivers, agentless calls, AgentRegistry restart, and root teardown. Composition, module-graph, build, and runtime-closure checks keep `ctx.agents` wired through the default bundle, SDK spine, Python runtime closure, and direct AgentLoop harnesses without another provider. + +Only a test-double host-aware transport consumes ambient identity; it derives `X-Harness-Session-Id` internally and verifies that tool schema and logged arguments contain no identity field. The service deliberately does not drain async work omitted from the Promise returned by the boundary operation; that work remains subject to its owner's explicit stop contract. + +## Alternatives considered + +**Pass Agent through every function.** Public, worker, process, persistence, and wire boundaries continue to do this, but requiring every process-local private helper to carry Agent adds repetitive forwarding without improving trust. ALS is confined to the asynchronous chain inside those explicit boundaries. + +**Make `ctx.agent` dynamic.** `ctx.agent` already means the static Agent associated with an Agent-scoped Cordis context. Changing the root meaning would mix registration and execution scopes and make concurrent behavior surprising. + +**Add a separate `ctx.agentExecution` service.** The carrier has no independent backend, configuration, or identity type: it stores the same `Agent` that `ctx.agents` already owns, and AgentLoop already depends on that service. A second mandatory provider would add package, composition, lifecycle, generated-catalog, and test-harness wiring without separating a real capability. + +**Store a named or complete runtime frame.** A one-field `{ agent }` frame only wraps the value, while Agent, Session, inbox, cancellation, turn, step, tool execution, and persistence already have authoritative owners. Adding more fields would create stale snapshots and another lifecycle; carrying `Agent` directly keeps the boundary named by its methods without duplicating state. + +**Include a step `AbortSignal`, `cwd`, sandbox, or authorization.** Their lifetimes and authority do not match the driver boundary, and their existing seams already pass them explicitly. Adding a control capability requires a separate decision and nested lifecycle contract. + +**Use a process-global `currentAgent`.** Concurrent Agents and subagents overwrite one another across awaited continuations, so a mutable global is correct only under a serialization guarantee the harness does not make. + +**Derive identity from model-visible arguments.** Model or user input cannot be trusted to select Session, tenant, or sandbox routing. + +**Add routing identity to every capability seam.** That spreads hosting concerns through provider-neutral APIs. A host-aware implementation owns its transport header while public boundaries remain explicit. + +## Consequences + +Deep infrastructure gains one trusted process-local initiating Agent without widening existing tool and capability requests. Concurrent and nested drivers isolate automatically, AgentLoop gains no additional mandatory service, and HMR/root disposal reaches quiescence before ALS is disabled. + +The dependency is implicit in function signatures and carries a capability-bearing Agent object. Consumers must restrict it to cross-cutting infrastructure, treat ambient presence as neither liveness nor authorization, and retain explicit cancellation and ownership checks. ALS also has an always-on propagation cost and does not cross worker, process, HTTP, or durable queue boundaries. + +The teardown design deliberately accepts Node's [Stability 1 (Experimental)](https://nodejs.org/api/async_context.html#asynclocalstoragedisable) `AsyncLocalStorage.disable()` dependency. Node requires `disable()` before an ALS instance can be garbage-collected, which matters when HMR replaces AgentRegistry-owned instances; the service state guard prevents a later boundary from re-entering the instance after disposal. + +The scope deliberately carries only the Agent, omitting turn, step, `signal`, `cwd`, sandbox, and authorization. A real consumer that cannot use existing explicit fields must justify any refinement separately; a stale copied field may at most mislabel telemetry, never grant control. diff --git a/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md new file mode 100644 index 0000000000..29ea3a47f4 --- /dev/null +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.zh.md @@ -0,0 +1,63 @@ +# RFC: 基于 AsyncLocalStorage 的发起 Agent 作用域 + +Status: implemented + +[English](2026-07-15-agent-initiator-scope.md) | 中文 + +## 问题 + +Harness 中存在两种有用但不同的上下文概念。Cordis `Context` 负责选择服务、注册归属和生命周期;`agent.ctx` 是一个存活 Agent 所拥有的扁平注册作用域。Agent 与会话身份描述的则是异步操作主体。若把根 `ctx.agent` 改成「当前正在运行的 Agent」,就会混淆这两种含义,并在单进程并发驱动多个 Agent 时失效。 + +进程内深层基础设施有时需要在显式传递的循环、工具及请求参数之下获取可信的发起 Agent,例如宿主感知传输层、追踪辅助函数、日志器或网关客户端。要求每个私有辅助函数都转发 `agent` 会造成重复,而进程级可变槽会在跨 `await` 时发生并发错误。模型可见参数也不适用,因为模型不得选择可信的会话或路由请求头。该载体归 Agent 服务所有,而非模型可见的可选上下文。 + +## 决策 + +必需的 `ctx.agents` 服务使用 Node `AsyncLocalStorage` 携带发起 Agent。它直接存储同一个 `Agent`,不引入只有一个字段的帧;[核心数据目录](../../../core-data-structures/core.md#initiating-agent)标明了所携带的类型。 + +`currentInitiator()` 用于可选读取,`requireInitiator()` 抛出 `no initiating agent is active`,`withInitiator(agent, operation)` 保留操作返回的同步值或 Promise 本身。`withoutInitiator(operation)` 会建立清空边界,供不得继承 Agent 的工作使用。会话仍通过 `agent.session` 推导;轮次、步骤、工具调用、`signal`、模型、`cwd`、沙箱和授权继续由现有归属方管理。 + +`AgentLoop` 已经注入 `ctx.agents`,并用 `agents.withInitiator(agent, ...)` 包裹每个具体驱动的完整 `runLoop` 生命周期。因此,并发驱动使用彼此独立的存储,子驱动会遮蔽父驱动,子边界结束后父存储得到恢复。创建、持久化加载和尚未发布的 `setup(agentCtx)` 位于子驱动边界之外:由父 Agent 发起的创建使用父身份,而 `agentCtx.agent` 显式标识子 Agent。 + +隐式身份不会取代显式契约。`ToolExecution.agent`、`AssembleContext.agent`、`GenerateOptions.sessionId`、任务归属、父子请求、`ctx.agent`、`agentCtx.agent`、审批与 hook 主体、`cwd` 选择、取消、worker 和进程消息、持久化记录及协议身份都保持显式传递。远程边界会把所需身份写入类型化请求,因为 ALS 只在进程内有效。 + +`AgentRegistry` 管理一个有序的发起方生命周期。teardown 会先拒绝新边界;移除 `ctx.agents` 后,AgentLoop 等注入方开始排空,注册表随后等待活动的返回 Promise 边界,最后调用 `AsyncLocalStorage.disable()`。排空期间,进行中代码可通过保留的服务引用继续调用 `currentInitiator()` 和 `requireInitiator()`;dispose 后,发起方方法会抛出 `agent initiator scope is disposed`。根 Context dispose 可能并发启动同级 fiber 的 teardown,因此除 Cordis 依赖顺序外仍必须统计活动边界。 + +发起方作用域不负责管理脱离返回链的工作:注册表排空只跟踪 `withInitiator()` 或 `withoutInitiator()` 返回的 Promise。边界内创建的异步资源会继承其存储,直到自身结束或 ALS 被禁用;所属 seam 必须显式停止未纳入返回 Promise 的工作。Agent 所有前台工作会把完整生命周期纳入返回值,并保留显式取消契约。无关的定时器、队列和部署基础设施在 `withoutInitiator(operation)` 下启动;队列、worker、进程和协议边界必须序列化身份,不能期待 ALS 传播。 + +宿主感知的传输层可以从 `ctx.agents.requireInitiator().session.id` 推导由部署方拥有的 `X-Harness-Session-Id` 等请求头;模型可见 schema 和参数中不包含该请求头。本决策不让现有生产 MCP 或 Web 传输层采用此请求头。测试替身传输层用于证明可信边界,而不会把宿主路由策略分配给现有的提供方无关 seam。 + +本决策扩展 [Agent 注册作用域契约](2026-07-08-agent-scope-contexts.md)及其[运行时设计](2026-07-12-agent-scope-runtime-design.md),不会改变其中 `agent.ctx` 的静态含义。 + +## 验证 + +Agent 服务测试锁定可选与必需读取、同步值和跨 realm Promise 的引用身份、并发、嵌套及清空边界、同步抛错或 Promise 拒绝后的恢复、排空顺序及保留引用的错误。AgentLoop 集成测试锁定并发与嵌套驱动、无 Agent 调用、AgentRegistry 重启及根 Context 销毁。组合、模块图、构建及运行时闭包检查确保默认组合包、SDK 主干、Python 运行时闭包及直接 AgentLoop harness 通过 `ctx.agents` 完成接线,无需其他提供方。 + +只有测试替身形式的宿主感知传输层消费隐式身份;它在内部推导 `X-Harness-Session-Id`,并验证工具 schema 与记录参数都不包含身份字段。服务有意不排空边界操作所返回 Promise 之外的异步工作;这类工作仍由所属方的显式停止契约管理。 + +## 考虑过的替代方案 + +**在每个函数中传递 Agent。** 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会造成重复转发,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。 + +**让 `ctx.agent` 变成动态值。** `ctx.agent` 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。 + +**新增独立的 `ctx.agentExecution` 服务。** 该载体没有独立后端、配置或身份类型:它存储的是 `ctx.agents` 已经管理的同一个 `Agent`,而 AgentLoop 本就依赖该服务。第二个必需提供方会增加包、组合、生命周期、生成目录及测试 harness 接线,却没有拆出真实能力。 + +**保存命名帧或完整运行时帧。** 只有一个字段的 `{ agent }` 帧只是包装该值,而 Agent、会话、inbox、取消、轮次、步骤、工具执行和持久化已经有各自的真源。增加更多字段会产生陈旧快照和另一套生命周期;直接携带 `Agent`,由方法名标识边界,无需重复保存状态。 + +**包含步骤级 `AbortSignal`、`cwd`、沙箱或授权。** 它们的生命周期及权限范围与驱动边界不一致,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期契约。 + +**使用进程级 `currentAgent`。** 并发 Agent 和 subagent 会在异步延续执行之间相互覆盖,因此可变全局值只在 Harness 不具备的串行保证下才正确。 + +**从模型可见参数推导身份。** 不能信任模型或用户输入来选择会话、租户或沙箱路由。 + +**给每个能力 seam 增加路由身份。** 这会把宿主关注点扩散到提供方无关 API。宿主感知实现拥有其传输请求头,而公开边界继续显式传递身份。 + +## 后果 + +深层基础设施可以获得一个可信的进程内发起 Agent,而无需加宽现有工具和能力请求。并发及嵌套驱动会自动隔离,AgentLoop 不增加新的必需服务,HMR 或根 Context dispose 会在禁用 ALS 前完成排空。 + +该依赖不会出现在函数签名中,并且携带一个具有控制能力的 Agent 对象。消费方必须将其限制在横切基础设施中,把隐式存在视为既不证明存活、也不授予权限,并保留显式取消和归属检查。ALS 还有常驻传播成本,也无法跨越 worker、进程、HTTP 或持久化队列边界。 + +该销毁设计有意依赖 Node 的 [Stability 1(实验性)](https://nodejs.org/api/async_context.html#asynclocalstoragedisable) API `AsyncLocalStorage.disable()`。Node 要求在 ALS 实例可被垃圾回收前调用 `disable()`,这对 HMR 替换 AgentRegistry 所拥有的实例尤为重要;服务状态守卫会阻止 dispose 后通过后续边界重新进入该实例。 + +该作用域有意只携带 Agent,省略轮次、步骤、`signal`、`cwd`、沙箱和授权。若真实消费方无法使用现有显式字段,必须另行论证扩展;陈旧字段最多只能误标遥测数据,绝不能授予控制权。 diff --git a/examples/coding-agent/tests/code-mode.e2e.ts b/examples/coding-agent/tests/code-mode.e2e.ts index e243270e29..a5e69b85ea 100644 --- a/examples/coding-agent/tests/code-mode.e2e.ts +++ b/examples/coding-agent/tests/code-mode.e2e.ts @@ -9,7 +9,6 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { RUN_CODE_NAME } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as ToolBash from '@deepseek-ai/dsh-tool-bash' @@ -49,7 +48,6 @@ async function codeModeHarness(cwd: string): Promise { await harness.plugin(SystemPrompt, { persona: PERSONA }) await harness.plugin(ToolRegistry, { mode: 'code' }) await harness.plugin(AgentRegistry) - await harness.plugin(AgentExecutionProvider) await harness.plugin(AgentLoop, { agents: [] }) await harness.plugin(LlmDeepSeek) await harness.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 }) @@ -68,7 +66,6 @@ async function workspaceCodeModeHarness(): Promise { await harness.plugin(LocalFileSystem, { cwd: '/' }) await harness.plugin(ToolFs) await harness.plugin(WorkspaceContext, { maxBytes: 65536 }) - await harness.plugin(AgentExecutionProvider) await harness.plugin(AgentLoop, { agents: [] }) await harness.plugin(LlmDeepSeek, { models: [{ id: 'deepseek-v4-flash' }] }) await harness.plugin(WorkerCodeRuntime, {}) diff --git a/packages/context/workspace-context/package.json b/packages/context/workspace-context/package.json index b6ff240cf1..7f704c838a 100644 --- a/packages/context/workspace-context/package.json +++ b/packages/context/workspace-context/package.json @@ -36,7 +36,6 @@ "devDependencies": { "@cordisjs/plugin-loader": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-fs": "workspace:^", "@deepseek-ai/dsh-fs-local": "workspace:^", diff --git a/packages/context/workspace-context/tests/workspace-context.e2e.ts b/packages/context/workspace-context/tests/workspace-context.e2e.ts index 3710f77547..0c11da8ed4 100644 --- a/packages/context/workspace-context/tests/workspace-context.e2e.ts +++ b/packages/context/workspace-context/tests/workspace-context.e2e.ts @@ -9,7 +9,6 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import * as WorkspaceContext from '@deepseek-ai/dsh-workspace-context' @@ -44,7 +43,6 @@ async function harness(): Promise<{ ctx: Context; agent: Agent }> { await ctx.plugin(LocalFileSystem, { cwd: '/' }) await ctx.plugin(ToolFs) await ctx.plugin(WorkspaceContext, { maxBytes: 65536 }) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LlmDeepSeek, { models: [{ id: 'deepseek-v4-flash' }] }) const handle = await ctx.agents.create({ diff --git a/packages/context/workspace-context/tests/workspace-context.spec.ts b/packages/context/workspace-context/tests/workspace-context.spec.ts index f161ac0bc9..2ff2067cf5 100644 --- a/packages/context/workspace-context/tests/workspace-context.spec.ts +++ b/packages/context/workspace-context/tests/workspace-context.spec.ts @@ -8,7 +8,6 @@ import * as workspaceContext from '@deepseek-ai/dsh-workspace-context' import LlmService, { CallId, type Message, type StreamChunk } from '@deepseek-ai/dsh-llm' import SessionStore, { Session, SessionId, SESSION_FORMAT_VERSION, type SessionEvent } from '@deepseek-ai/dsh-session' import AgentRegistry, { type Agent, type HookContext } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { FileSystem, FsTargetKey, FsVersion } from '@deepseek-ai/dsh-fs' import type { @@ -1587,7 +1586,6 @@ describe('dynamic nested workspace context injection', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(LocalFileSystem, { cwd: '/' }) await ctx.plugin(ToolFs) await ctx.plugin(workspaceContext, { dshHome: home, maxBytes: 65536 }) diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 4ff01c81ae..5d99506c8a 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -52,15 +52,6 @@ export interface TypeApiEntry { /** Every harness `ctx.` service, sorted by key. */ export const SERVICE_API: readonly ServiceApiEntry[] = [ - { - key: 'agentExecution', - summary: 'Ambient Agent identity within one process-local asynchronous chain.', - methods: [ - 'current(): AgentExecution | undefined', - 'require(): AgentExecution', - 'run(execution: AgentExecution | undefined, operation: () => T): T', - ], - }, { key: 'agentLoop', summary: 'Concrete agent factory and driver service.', @@ -72,8 +63,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [ }, { key: 'agents', - summary: 'Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package.', + summary: 'Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain.', methods: [ + 'currentInitiator(): Agent | undefined', + 'requireInitiator(): Agent', + 'withInitiator(agent: Agent, operation: () => T): T', + 'withoutInitiator(operation: () => T): T', 'setFactory(factory: AgentFactory): () => void', 'async create(options: CreateAgentOptions): Promise', 'async resume(options: ResumeAgentOptions): Promise', @@ -552,10 +547,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'Agent', declaration: 'export interface Agent {\n readonly id: SessionId;\n readonly options: AgentOptions;\n readonly session: Session;\n readonly status: AgentStatus;\n readonly ctx: Context;\n send(content: ContentBlock[], options?: SendOptions): void;\n steer(content: ContentBlock[], options?: SendOptions): void;\n inject(content: ContentBlock[], options?: InjectOptions): void;\n cancel(reason?: string): void;\n whenIdle(): Promise;\n}', }, - { - name: 'AgentExecution', - declaration: 'export interface AgentExecution {\n readonly agent: Agent;\n}', - }, { name: 'AgentFactory', declaration: 'export interface AgentFactory {\n createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise;\n resume(ownerCtx: Context, options: ResumeAgentOptions): Promise;\n}', diff --git a/packages/core/README.md b/packages/core/README.md index 3c0ad38819..fe98a78c22 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -8,12 +8,11 @@ The session log, system-prompt assembly, tool registry, agent vocabulary, and co | `session/` | Event-sourced session log + in-memory store | `ctx.sessions` | | `system-prompt/` | Prompt-section + tool-schema assembly registry | `ctx.systemPrompt` | | `tools/` | Scoped tool registry + pre-policy, guards, around-dispatch, post-policy, and final-result observation | `ctx.tools` | -| `agent/` | Agent interface, registry, `agent/*` event vocabulary | `ctx.agents` | -| `agent-execution/` | Process-local ambient Agent identity for asynchronous driver work | `ctx.agentExecution` | +| `agent/` | Agent interface, live registry, process-local initiator scope, `agent/*` event vocabulary | `ctx.agents` | | `agent-loop/` | Concrete plugin implementing the public `Agent` contract and owning the loop driver | `ctx.agentLoop` | `scope/` is the one non-service package here: a dependency-free library (`createScope`/`scopeOf`/`scopeTarget`) the registries and the loop build per-agent scoping on — it sits below `session/` and `system-prompt/` in the module graph precisely so they can consume it without a cycle. -`agent-execution` is mandatory control infrastructure for concrete loops and deep process-local consumers. `agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop. Extension plugins depend on `agent` and, when they need ambient identity, `agent-execution`; they never depend on `agent-loop` directly, so the loop stays swappable. +`agent-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop. It runs each driver inside `ctx.agents.withInitiator()`. Extension plugins depend on `agent`, including when they need the initiating Agent, and never on `agent-loop` directly, so the loop stays swappable. -The default composition that wires this spine into a runnable agent lives in [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md): one bundle plugin that loads the control spine plus selected default capabilities (`timer` + `llm` + sessions + system-prompt + tools + agents + agent-execution + invariants + the local [skill family](../skill/README.md) + `tool-bash` + workspace-context + `agent-loop`) and forwards `agent-loop`'s `agents` list as its own config. It sits in `examples/` — ready-to-run demo/reference bundles — not in `core/`: `core/` ships the swappable spine pieces, while a demo bundle picks one concrete composition of them and adds a front door. +The default composition that wires this spine into a runnable agent lives in [`examples/agent-spine-demo`](../examples/agent-spine-demo/README.md): one bundle plugin that loads the control spine plus selected default capabilities (`timer` + `llm` + sessions + system-prompt + tools + agents + invariants + the local [skill family](../skill/README.md) + `tool-bash` + workspace-context + `agent-loop`) and forwards `agent-loop`'s `agents` list as its own config. It sits in `examples/` — ready-to-run demo/reference bundles — not in `core/`: `core/` ships the swappable spine pieces, while a demo bundle picks one concrete composition of them and adds a front door. diff --git a/packages/core/agent-execution/README.md b/packages/core/agent-execution/README.md deleted file mode 100644 index 8c01f0b3c4..0000000000 --- a/packages/core/agent-execution/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# dsh-agent-execution - -Process-local ambient Agent identity for asynchronous work initiated by a concrete agent driver. The default export, `AgentExecutionProvider`, installs the mandatory `ctx.agentExecution` service; [`dsh-agent-loop`](../agent-loop/README.md) establishes one boundary around each driver's complete lifetime. - -## Service: `AgentExecutionService` (ctx key: `agentExecution`) - -- `current()` returns the inherited `AgentExecution` or `undefined` outside a driver and inside an explicit clearing boundary. -- `require()` returns the inherited execution or throws `no agent execution context is active`. -- `run(execution, operation)` returns the exact synchronous value or Promise from `operation`. Passing `undefined` establishes a real boundary that hides an inherited Agent. - -The store contains only `{ readonly agent: Agent }`. The `Session` remains available through `agent.session`; turn, step, `signal`, `cwd`, sandbox, authorization, and other capability state remain with their explicit owners. Ambient presence identifies the initiator but does not prove that the Agent is live or that an operation is authorized. - -## Lifetime and detached work - -Provider teardown rejects new `run()` boundaries, removes the service so injected dependents drain, waits for returned Promise boundaries, then disables its `AsyncLocalStorage`. In-flight code retaining the service can call `current()` and `require()` while it drains; after disposal, all three methods throw `agent execution service is disposed`. - -Async resources created inside `run()` inherit its Agent even if `operation` returns before they settle, but provider teardown waits only for the Promise returned by `operation`. The owning seam must stop unreturned work explicitly. Unrelated timers, queues, and deployment infrastructure start under `run(undefined, operation)` and own an explicit stop; queue, worker, process, and wire boundaries serialize any identity they need instead of relying on ALS propagation. - -## Known Limitations and Deferred Work - -- **Process-local only** — ALS does not cross workers, child processes, HTTP, durable queues, or restarts; each boundary materializes a typed identity explicitly. -- **Agent identity only** — turn, step, signal, cwd, sandbox, and authorization stay outside the frame until a concrete cross-cutting consumer justifies a separate design. -- **Ambient references may outlive liveness** — consumers still check `agent.status`, their explicit signal, and the owning capability contract before lifecycle-sensitive work. diff --git a/packages/core/agent-execution/package.json b/packages/core/agent-execution/package.json deleted file mode 100644 index 7c2c527d3d..0000000000 --- a/packages/core/agent-execution/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@deepseek-ai/dsh-agent-execution", - "description": "Process-local ambient Agent context for asynchronous driver work", - "version": "0.0.1", - "private": true, - "type": "module", - "main": "lib/index.js", - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/index.d.ts", - "default": "./lib/index.js" - }, - "./package.json": "./package.json" - }, - "files": [ - "lib/index.js", - "lib/types/**/*.d.ts", - "lib/types/**/*.d.ts.map", - "src" - ], - "license": "BSD-3-Clause", - "peerDependencies": { - "@deepseek-ai/dsh-agent": "^0.0.1", - "cordis": "^4.0.0-rc.7" - }, - "devDependencies": { - "@deepseek-ai/dsh-agent": "workspace:^", - "cordis": "^4.0.0-rc.7" - } -} diff --git a/packages/core/agent-execution/src/index.ts b/packages/core/agent-execution/src/index.ts deleted file mode 100644 index cb2683dbaa..0000000000 --- a/packages/core/agent-execution/src/index.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Process-local Agent execution context backed by Node AsyncLocalStorage. - * - * @module @deepseek-ai/dsh-agent-execution - */ - -import type { Context } from 'cordis' -import { AsyncLocalStorage } from 'node:async_hooks' -import { isPromise } from 'node:util/types' -import type { AgentExecution } from './types.ts' - -export type { AgentExecution } from './types.ts' - -const NO_ACTIVE_EXECUTION = 'no agent execution context is active' -const DISPOSED_SERVICE = 'agent execution service is disposed' - -/** Ambient Agent identity within one process-local asynchronous chain. */ -export interface AgentExecutionService { - /** - * Read the active execution without requiring one. - * @returns the inherited execution, or `undefined` outside/inside a cleared boundary. - * @throws when this service instance has been disposed. - */ - current(): AgentExecution | undefined - - /** - * Read the active execution and fail when no boundary is active. - * @returns the inherited execution. - * @throws when no execution is active or this service instance has been disposed. - */ - require(): AgentExecution - - /** - * Run an operation inside an execution boundary. Passing `undefined` clears - * an inherited execution; the exact synchronous value or Promise is returned. - * @param execution - execution to inherit, or `undefined` for a clearing boundary. - * @param operation - synchronous or asynchronous operation to invoke. - * @returns the exact value returned by `operation`. - * @throws when this service is closing/disposed, or when `operation` throws. - */ - run(execution: AgentExecution | undefined, operation: () => T): T -} - -declare module 'cordis' { - interface Context { - agentExecution: AgentExecutionService - } -} - -/** One provider-owned ALS instance with quiescent shutdown. */ -class DefaultAgentExecutionService implements AgentExecutionService { - private readonly storage = new AsyncLocalStorage() - private state: 'active' | 'closing' | 'disposed' = 'active' - private activeRuns = 0 - private drainWaiter: PromiseWithResolvers | undefined - private disposalTask: Promise | undefined - - current(): AgentExecution | undefined { - this.assertReadable() - return this.storage.getStore() - } - - require(): AgentExecution { - const execution = this.current() - if (execution === undefined) throw new Error(NO_ACTIVE_EXECUTION) - return execution - } - - run(execution: AgentExecution | undefined, operation: () => T): T { - if (this.state !== 'active') throw new Error(DISPOSED_SERVICE) - this.activeRuns += 1 - let result: T - try { - result = this.storage.run(execution, operation) - } catch (error: unknown) { - this.releaseRun() - throw error - } - if (isPromise(result)) { - void result.then( - () => { this.releaseRun() }, - () => { this.releaseRun() }, - ) - } else { - this.releaseRun() - } - return result - } - - /** Reject new boundaries while existing continuations remain readable. */ - close(): void { - if (this.state === 'active') this.state = 'closing' - } - - /** Wait for every returned Promise boundary, then invalidate retained references. */ - dispose(): Promise { - return (this.disposalTask ??= (async () => { - this.close() - if (this.activeRuns !== 0) { - this.drainWaiter ??= Promise.withResolvers() - await this.drainWaiter.promise - } - this.state = 'disposed' - this.storage.disable() - })()) - } - - private assertReadable(): void { - if (this.state === 'disposed') throw new Error(DISPOSED_SERVICE) - } - - private releaseRun(): void { - this.activeRuns -= 1 - if (this.activeRuns !== 0) return - this.drainWaiter?.resolve() - this.drainWaiter = undefined - } -} - -/** Cordis provider for the mandatory `ctx.agentExecution` service. */ -export class AgentExecutionProvider { - private readonly service = new DefaultAgentExecutionService() - - /** - * Install one isolated execution service and its ordered lifecycle. - * @param ctx - provider-owning Cordis context. - */ - constructor(ctx: Context) { - const service = this.service - ctx.effect(function* () { - // First yielded, disposed last: invalidate ALS only after dependents and active runs drain. - yield () => service.dispose() - yield ctx.provide('agentExecution', service) - // Last yielded, disposed first: prevent a teardown race from opening another boundary. - yield () => { service.close() } - }, 'agentExecution.lifecycle()') - } -} - -export default AgentExecutionProvider diff --git a/packages/core/agent-execution/src/types.ts b/packages/core/agent-execution/src/types.ts deleted file mode 100644 index 94b144785b..0000000000 --- a/packages/core/agent-execution/src/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Public Agent execution-context types. - * - * @module @deepseek-ai/dsh-agent-execution/types - */ - -import type { Agent } from '@deepseek-ai/dsh-agent' - -/** The exact Agent associated with one asynchronous execution chain. */ -export interface AgentExecution { - readonly agent: Agent -} diff --git a/packages/core/agent-execution/tests/agent-execution.spec.ts b/packages/core/agent-execution/tests/agent-execution.spec.ts deleted file mode 100644 index b18d19e63d..0000000000 --- a/packages/core/agent-execution/tests/agent-execution.spec.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { Context } from 'cordis' -import { runInNewContext } from 'node:vm' -import type { Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' -import type { AgentExecution, AgentExecutionService } from '@deepseek-ai/dsh-agent-execution' -import { SessionId } from '@deepseek-ai/dsh-session' - -function execution(id: string): AgentExecution { - return { agent: { id: SessionId(id) } as Agent } -} - -async function harness(): Promise<{ - ctx: Context - service: AgentExecutionService - dispose: () => Promise -}> { - const ctx = new Context() - const fiber = await ctx.plugin(AgentExecutionProvider) - return { - ctx, - service: ctx.agentExecution, - dispose: fiber.dispose, - } -} - -describe('AgentExecutionProvider', () => { - it('reports an absent boundary and requires an active execution', async () => { - const { service, dispose } = await harness() - expect(service.current()).toBeUndefined() - expect(() => service.require()).toThrow('no agent execution context is active') - await dispose() - }) - - it('preserves exact synchronous and Promise return identities across await', async () => { - const { service, dispose } = await harness() - const active = execution('identity') - const value = { result: true } - expect(service.run(active, () => { - expect(service.require()).toBe(active) - return value - })).toBe(value) - - const promise = service.run(active, async () => { - expect(service.require()).toBe(active) - await Promise.resolve() - expect(service.require()).toBe(active) - return value - }) - expect(service.run(active, () => promise)).toBe(promise) - await expect(promise).resolves.toBe(value) - expect(service.current()).toBeUndefined() - await dispose() - }) - - it('isolates overlapping executions', async () => { - const { service, dispose } = await harness() - const a = execution('a') - const b = execution('b') - const bothStarted = Promise.withResolvers() - const release = Promise.withResolvers() - let starts = 0 - const run = (active: AgentExecution): Promise => service.run(active, async () => { - expect(service.require()).toBe(active) - starts += 1 - if (starts === 2) bothStarted.resolve(true) - await release.promise - expect(service.require()).toBe(active) - }) - - const pending = [run(a), run(b)] - await bothStarted.promise - expect(service.current()).toBeUndefined() - release.resolve(true) - await Promise.all(pending) - await dispose() - }) - - it('restores nested and explicitly cleared boundaries', async () => { - const { service, dispose } = await harness() - const parent = execution('parent') - const child = execution('child') - - service.run(parent, () => { - expect(service.require()).toBe(parent) - service.run(child, () => { expect(service.require()).toBe(child) }) - expect(service.require()).toBe(parent) - service.run(undefined, () => { - expect(service.current()).toBeUndefined() - expect(() => service.require()).toThrow('no agent execution context is active') - }) - expect(service.require()).toBe(parent) - }) - expect(service.current()).toBeUndefined() - await dispose() - }) - - it('restores context after synchronous throws and rejected operations', async () => { - const { service, dispose } = await harness() - const parent = execution('parent') - const child = execution('child') - const syncError = new Error('sync failure') - const asyncError = new Error('async failure') - - service.run(parent, () => { - expect(() => service.run(child, () => { throw syncError })).toThrow(syncError) - expect(service.require()).toBe(parent) - }) - await expect(service.run(child, async () => { - await Promise.resolve() - throw asyncError - })).rejects.toBe(asyncError) - expect(service.current()).toBeUndefined() - await dispose() - }) - - it('stops new boundaries, drains active Promises, and invalidates retained references', async () => { - const { ctx, service, dispose } = await harness() - const active = execution('draining') - const release = Promise.withResolvers() - const pending = service.run(active, async () => { - await release.promise - expect(service.require()).toBe(active) - }) - let disposed = false - const disposal = dispose().then(() => { disposed = true }) - await Promise.resolve() - - expect(() => service.run(active, () => 1)).toThrow('agent execution service is disposed') - expect(disposed).toBe(false) - expect(ctx.get('agentExecution')).toBeUndefined() - release.resolve(true) - await pending - await disposal - expect(() => service.current()).toThrow('agent execution service is disposed') - expect(() => service.require()).toThrow('agent execution service is disposed') - }) - - it('drains cross-realm Promise boundaries before disposal', async () => { - const { service, dispose } = await harness() - const active = execution('cross-realm') - const release = Promise.withResolvers() - const operation = runInNewContext( - '(async () => { await release; inspect() })', - { - release: release.promise, - inspect: () => { expect(service.require()).toBe(active) }, - }, - ) as () => Promise - const pending = service.run(active, operation) - expect(pending).not.toBeInstanceOf(Promise) - - let disposed = false - const disposal = dispose().then(() => { disposed = true }) - await Promise.resolve() - expect(disposed).toBe(false) - - release.resolve(true) - await pending - await disposal - expect(disposed).toBe(true) - }) -}) diff --git a/packages/core/agent-execution/tsconfig.json b/packages/core/agent-execution/tsconfig.json deleted file mode 100644 index a06784e926..0000000000 --- a/packages/core/agent-execution/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "rootDir": "src", - "outDir": "lib/types" - }, - "include": [ - "src" - ], - "references": [ - { - "path": "../../../vendor/cosmokit" - }, - { - "path": "../../../vendor/cordis" - }, - { - "path": "../../core/agent" - } - ] -} diff --git a/packages/core/agent-loop/README.md b/packages/core/agent-loop/README.md index f45411923c..db5861f8c1 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -25,7 +25,7 @@ The config-driven `ctx.agentLoop.create()` path keeps its agent owned by the loo ### Injected services -`agents`, `agentExecution`, `sessions`, `llm`, `tools`, `systemPrompt` — all six interface services. The loop cannot activate without `agentExecution`; the default bundle loads its provider before the loop. +`agents`, `sessions`, `llm`, `tools`, `systemPrompt` — all five interface services. ### Configuration (schemastery) @@ -50,7 +50,7 @@ The concrete `Agent` class, its `Inbox`, `runLoop`, and instance-bound publicati ### Loop lifecycle (`loop.ts`) -The driver owns one agent for its lifetime and runs inside `ctx.agentExecution.run({ agent }, ...)`, so process-local asynchronous continuations can recover the initiating Agent. Creation, persistence load, and unpublished setup stay outside the driver boundary; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. The [execution-context package](../agent-execution/README.md) owns propagation, teardown, and detached-work rules. +The driver owns one agent for its lifetime and runs inside `ctx.agents.withInitiator(agent, ...)`, so process-local asynchronous continuations can recover the initiating Agent. Creation, persistence load, and unpublished setup stay outside the driver boundary; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. The [agent service](../agent/README.md#initiating-agent-scope) owns propagation, teardown, and detached-work rules. Every provider call that reaches a successful finish appends exactly one `assistant/message` completion anchor, including content-less calls and `max-tokens` finishes. A successful `agent/step-result` stores its transformed content; a rejected result records empty content before the original failure continues. The anchor retains exact chunk provenance (`[]` for a stream with no chunks) and usage when available, while empty content stays out of derived message history. diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 6a7fe2d937..7e2fb235a2 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -22,7 +22,6 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", - "@deepseek-ai/dsh-agent-execution": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-scope": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", @@ -36,7 +35,6 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", diff --git a/packages/core/agent-loop/src/agent.ts b/packages/core/agent-loop/src/agent.ts index 67c9160332..6ab02bfc13 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -387,7 +387,7 @@ export class ReactLoopAgent implements Agent { [startDriver](): void { if (this._status === 'disposed') return this.driverStarted = true - this.done = this.loopCtx.agentExecution.run({ agent: this }, () => runLoop(this.loopCtx, this, { + this.done = this.loopCtx.agents.withInitiator(this, () => runLoop(this.loopCtx, this, { inbox: this.#inbox, maxParallelToolCalls: this.maxParallelToolCalls, setStatus: (status) => { this.setStatus(status) }, diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 6a0d714ef3..2a77afc983 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -11,7 +11,6 @@ import z from 'schemastery' import { createScope } from '@deepseek-ai/dsh-scope' import type { Scope } from '@deepseek-ai/dsh-scope' import { agentEvents } from '@deepseek-ai/dsh-agent' -import type {} from '@deepseek-ai/dsh-agent-execution' import type { Agent, AgentFactory, @@ -406,7 +405,7 @@ function validateConfiguredAgents(agents: Config['agents']): void { /** Concrete agent factory and driver service. */ export class AgentLoop extends Service implements AgentFactory { - static inject = ['agents', 'agentExecution', 'sessions', 'llm', 'tools', 'systemPrompt'] + static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt'] /** Runtime schema for declarative agents. */ static Config = z.object({ diff --git a/packages/core/agent-loop/tests/agent-execution.spec.ts b/packages/core/agent-loop/tests/agent-initiator.spec.ts similarity index 76% rename from packages/core/agent-loop/tests/agent-execution.spec.ts rename to packages/core/agent-loop/tests/agent-initiator.spec.ts index ee6f2d94c7..7f0903fc00 100644 --- a/packages/core/agent-loop/tests/agent-execution.spec.ts +++ b/packages/core/agent-loop/tests/agent-initiator.spec.ts @@ -1,8 +1,6 @@ import { describe, expect, it } from 'vitest' -import { Context, FiberState, type Fiber } from 'cordis' +import { Context, type Fiber } from 'cordis' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' -import type { AgentExecutionService } from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' @@ -13,7 +11,7 @@ import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' interface Harness { ctx: Context - providerFiber: Fiber + agentsFiber: Fiber loopFiber: Fiber } @@ -23,11 +21,10 @@ async function harness(adapter: LlmAdapter): Promise { await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) - await ctx.plugin(AgentRegistry) - const providerFiber = await ctx.plugin(AgentExecutionProvider) + const agentsFiber = await ctx.plugin(AgentRegistry) const loopFiber = await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) - return { ctx, providerFiber, loopFiber } + return { ctx, agentsFiber, loopFiber } } function waitForIdle(ctx: Context, agent: Agent): Promise { @@ -56,12 +53,12 @@ class OverlapAdapter extends LlmAdapter { } async * stream(options: GenerateOptions): AsyncIterable { - const before = this.ctx.agentExecution.require().agent + const before = this.ctx.agents.requireInitiator() this.starts += 1 if (this.starts === 2) this.bothStarted.resolve(true) await this.bothStarted.promise await Promise.resolve() - const after = this.ctx.agentExecution.require().agent + const after = this.ctx.agents.requireInitiator() this.observations.push({ sessionId: options.sessionId, before, after }) yield* textResponse('done') } @@ -71,12 +68,12 @@ class OverlapAdapter extends LlmAdapter { class TestCapabilityTransport { readonly requests: { path: string; headers: Record }[] = [] - constructor(private readonly execution: AgentExecutionService) {} + constructor(private readonly agents: AgentRegistry) {} async request(path: string): Promise> { await Promise.resolve() const headers = { - 'X-Harness-Session-Id': this.execution.require().agent.session.id, + 'X-Harness-Session-Id': this.agents.requireInitiator().session.id, } this.requests.push({ path, headers }) return headers @@ -89,11 +86,11 @@ class ReloadAdapter extends LlmAdapter { firstAgentDuringAbort: Agent | undefined laterAgent: Agent | undefined calls = 0 - execution: AgentExecutionService | undefined + agents: AgentRegistry | undefined async * stream(options: GenerateOptions): AsyncIterable { - const execution = this.execution - if (execution === undefined) throw new Error('execution service missing') + const agents = this.agents + if (agents === undefined) throw new Error('agent service missing') this.calls += 1 if (this.calls === 1) { this.firstStarted.resolve(true) @@ -105,18 +102,18 @@ class ReloadAdapter extends LlmAdapter { }) } catch (error: unknown) { await Promise.resolve() - this.firstAgentDuringAbort = execution.require().agent + this.firstAgentDuringAbort = agents.requireInitiator() throw error } return } await Promise.resolve() - this.laterAgent = execution.require().agent + this.laterAgent = agents.requireInitiator() yield* textResponse('reloaded') } } -describe('AgentLoop execution context', () => { +describe('AgentLoop initiator scope', () => { it('keeps overlapping driver continuations bound to their exact Agents', async () => { const ctx = new Context() const adapter = new OverlapAdapter(ctx) @@ -125,7 +122,6 @@ describe('AgentLoop execution context', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) @@ -142,7 +138,7 @@ describe('AgentLoop execution context', () => { { sessionId: a.session.id, before: a, after: a }, { sessionId: b.session.id, before: b, after: b }, ])) - expect(ctx.agentExecution.current()).toBeUndefined() + expect(ctx.agents.currentInitiator()).toBeUndefined() await ctx.fiber.dispose() }) @@ -170,7 +166,7 @@ describe('AgentLoop execution context', () => { sessionId: SessionId('child-session'), agentOptions: { provider: 'mock', model: 'mock' }, setup: (agentCtx) => { - parentDuringSetup = ctx.agentExecution.require().agent + parentDuringSetup = ctx.agents.requireInitiator() explicitChild = agentCtx.agent agentCtx.tools.register(defineTool({ name: 'observe-child', @@ -178,7 +174,7 @@ describe('AgentLoop execution context', () => { parameters: {}, execute: async () => { await Promise.resolve() - childDuringDriver = ctx.agentExecution.require().agent + childDuringDriver = ctx.agents.requireInitiator() return [{ type: 'text', text: 'observed' }] }, })) @@ -187,7 +183,7 @@ describe('AgentLoop execution context', () => { child = handle.agent send(handle.agent, 'run child') await handle.agent.whenIdle() - parentAfterChild = ctx.agentExecution.require().agent + parentAfterChild = ctx.agents.requireInitiator() await handle.dispose() return [{ type: 'text', text: 'child completed' }] }, @@ -205,7 +201,7 @@ describe('AgentLoop execution context', () => { expect(explicitChild).toBe(child) expect(childDuringDriver).toBe(child) expect(parentAfterChild).toBe(parentHandle.agent) - expect(ctx.agentExecution.current()).toBeUndefined() + expect(ctx.agents.currentInitiator()).toBeUndefined() await parentHandle.dispose() await ctx.fiber.dispose() }) @@ -216,7 +212,7 @@ describe('AgentLoop execution context', () => { textResponse('done'), ]) const { ctx } = await harness(adapter) - const transport = new TestCapabilityTransport(ctx.agentExecution) + const transport = new TestCapabilityTransport(ctx.agents) let directAmbient: Agent | undefined let captured: Agent | undefined @@ -226,7 +222,7 @@ describe('AgentLoop execution context', () => { parameters: {}, execute: async () => { await Promise.resolve() - directAmbient = ctx.agentExecution.current()?.agent + directAmbient = ctx.agents.currentInitiator() return [{ type: 'text', text: 'ok' }] }, })) @@ -235,7 +231,7 @@ describe('AgentLoop execution context', () => { description: 'call the test capability transport', parameters: { path: { type: 'string' } }, execute: async (args) => { - captured = ctx.agentExecution.require().agent + captured = ctx.agents.requireInitiator() const path = (args as { path: string }).path const headers = await transport.request(path) return [{ type: 'text', text: JSON.stringify(headers) }] @@ -271,43 +267,15 @@ describe('AgentLoop execution context', () => { await handle.dispose() expect(captured?.status).toBe('disposed') - expect(ctx.agentExecution.current()).toBeUndefined() + expect(ctx.agents.currentInitiator()).toBeUndefined() await ctx.fiber.dispose() }) - it('keeps AgentLoop inactive until the mandatory provider appears', async () => { - const ctx = new Context() - await ctx.plugin(LlmService) - await ctx.plugin(SessionStore) - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRegistry) - await ctx.plugin(AgentRegistry) - const loopFiber = ctx.plugin(AgentLoop, { agents: [] }) - await Promise.resolve() - expect(loopFiber.state).toBe(FiberState.PENDING) - - await ctx.plugin(AgentExecutionProvider) - await loopFiber - expect(loopFiber.state).toBe(FiberState.ACTIVE) - await ctx.fiber.dispose() - }) - - it('drains the old driver before disabling ALS during provider restart', async () => { - const ctx = new Context() + it('drains the old driver before disabling ALS during agent-service restart', async () => { const adapter = new ReloadAdapter() - const { providerFiber, loopFiber } = await (async (): Promise => { - await ctx.plugin(LlmService) - await ctx.plugin(SessionStore) - await ctx.plugin(SystemPrompt) - await ctx.plugin(ToolRegistry) - await ctx.plugin(AgentRegistry) - const mountedProvider = await ctx.plugin(AgentExecutionProvider) - const mountedLoop = await ctx.plugin(AgentLoop, { agents: [] }) - ctx.llm.registerAdapter(['mock'], adapter) - return { ctx, providerFiber: mountedProvider, loopFiber: mountedLoop } - })() - const oldService = ctx.agentExecution - adapter.execution = oldService + const { ctx, agentsFiber, loopFiber } = await harness(adapter) + const oldService = ctx.agents + adapter.agents = oldService const oldHandle = await ctx.agents.create({ sessionId: SessionId('before-restart-session'), agentOptions: { provider: 'mock', model: 'mock' }, @@ -316,14 +284,14 @@ describe('AgentLoop execution context', () => { send(oldAgent, 'block') await adapter.firstStarted.promise - await providerFiber.restart() + await agentsFiber.restart() await loopFiber.await() expect(adapter.firstAgentDuringAbort?.id).toBe(oldAgent.id) expect(adapter.firstAgentDuringAbort?.session).toBe(oldAgent.session) expect(oldAgent.status).toBe('disposed') - expect(() => oldService.current()).toThrow('agent execution service is disposed') - expect(ctx.agentExecution).not.toBe(oldService) - adapter.execution = ctx.agentExecution + expect(() => oldService.currentInitiator()).toThrow('agent initiator scope is disposed') + expect(ctx.agents).not.toBe(oldService) + adapter.agents = ctx.agents const newHandle = await ctx.agents.create({ sessionId: SessionId('after-restart-session'), @@ -346,11 +314,10 @@ describe('AgentLoop execution context', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) - const service = ctx.agentExecution - adapter.execution = service + const service = ctx.agents + adapter.agents = service const handle = await ctx.agents.create({ sessionId: SessionId('root-dispose-session'), agentOptions: { provider: 'mock', model: 'mock' }, @@ -363,6 +330,6 @@ describe('AgentLoop execution context', () => { expect(adapter.firstAgentDuringAbort?.id).toBe(agent.id) expect(adapter.firstAgentDuringAbort?.session).toBe(agent.session) expect(agent.status).toBe('disposed') - expect(() => service.current()).toThrow('agent execution service is disposed') + expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed') }) }) diff --git a/packages/core/agent-loop/tests/agent.spec.ts b/packages/core/agent-loop/tests/agent.spec.ts index 668190dd3d..d23b69efb7 100644 --- a/packages/core/agent-loop/tests/agent.spec.ts +++ b/packages/core/agent-loop/tests/agent.spec.ts @@ -5,7 +5,6 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from '@deepseek-ai/dsh-agent-loop' import { bindReactLoopAgentContext, prepareReactLoopAgent, type ReactLoopAgent } from '../src/agent.ts' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -21,7 +20,6 @@ async function harness(adapter: MockAdapter) { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx @@ -56,7 +54,6 @@ function send(agent: Agent, text: string) { describe('Agent', () => { it('rejects access before context binding and a second driver for one session', async () => { const ctx = new Context() - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(SessionStore) const session = ctx.sessions.create(SessionId('exclusive-driver')) const prepared = prepareReactLoopAgent( @@ -266,8 +263,8 @@ describe('Agent', () => { // test seam. Then call its disposer twice — the second call hits the // early-return branch. const ctx = new Context() - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(SessionStore) + await ctx.plugin(AgentRegistry) const session = ctx.sessions.create(SessionId('test')) const prepared = prepareReactLoopAgent( ctx, SessionId('bare'), { provider: 'mock', model: 'mock' }, session, DEFAULT_MAX_PARALLEL_TOOL_CALLS, @@ -386,7 +383,6 @@ describe('Agent', () => { // `done` (loop exit), not an eager resolve. A bare Agent + direct // internal driver disposer keeps the emit synchronous. const ctx = new Context() - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(LlmService) await ctx.plugin(SessionStore) await ctx.plugin(SystemPrompt) diff --git a/packages/core/agent-loop/tests/cancel.spec.ts b/packages/core/agent-loop/tests/cancel.spec.ts index e65ffc42a6..2d32388171 100644 --- a/packages/core/agent-loop/tests/cancel.spec.ts +++ b/packages/core/agent-loop/tests/cancel.spec.ts @@ -14,7 +14,6 @@ import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -29,7 +28,6 @@ async function harness(adapter: MockAdapter) { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx @@ -200,7 +198,6 @@ describe('Agent.cancel()', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) @@ -325,7 +322,6 @@ describe('Agent.cancel()', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) diff --git a/packages/core/agent-loop/tests/config-session-id.spec.ts b/packages/core/agent-loop/tests/config-session-id.spec.ts index 8710ee5629..cadc176aea 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -10,7 +10,6 @@ import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -32,7 +31,6 @@ async function makeCoreContext(): Promise { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) return ctx } @@ -308,7 +306,6 @@ describe('config-driven session id', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) const loopFiber = await ctx.plugin(AgentLoop, { agents: [{ id: SessionId('main'), provider: 'mock', model: 'mock', resumeSessionId: SessionId('deferred') }], }) @@ -331,7 +328,6 @@ describe('config-driven session id', () => { await ctx1.plugin(SystemPrompt) await ctx1.plugin(ToolRegistry) await ctx1.plugin(AgentRegistry) - await ctx1.plugin(AgentExecutionProvider) await ctx1.plugin(AgentLoop, { agents: [{ id: SessionId('cfg'), provider: 'mock', model: 'mock' }] }) await ctx1.plugin(SessionPersistenceJsonl, { root }) ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg')])) @@ -351,7 +347,6 @@ describe('config-driven session id', () => { await ctx2.plugin(SystemPrompt) await ctx2.plugin(ToolRegistry) await ctx2.plugin(AgentRegistry) - await ctx2.plugin(AgentExecutionProvider) await ctx2.plugin(AgentLoop, { agents: [{ id: SessionId('cfg'), provider: 'mock', model: 'mock' }] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg2')])) @@ -376,7 +371,6 @@ describe('config-driven session id', () => { await ctx1.plugin(SystemPrompt) await ctx1.plugin(ToolRegistry) await ctx1.plugin(AgentRegistry) - await ctx1.plugin(AgentExecutionProvider) await ctx1.plugin(AgentLoop, { agents: [] }) await ctx1.plugin(SessionPersistenceJsonl, { root }) ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('first')])) @@ -393,7 +387,6 @@ describe('config-driven session id', () => { await ctx2.plugin(SystemPrompt) await ctx2.plugin(ToolRegistry) await ctx2.plugin(AgentRegistry) - await ctx2.plugin(AgentExecutionProvider) await ctx2.plugin(AgentLoop, { agents: [{ id: SessionId('main'), provider: 'mock', model: 'mock', resumeSessionId: SessionId('sticky-1') }] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('second')])) @@ -423,7 +416,6 @@ describe('config-driven session id', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [{ id: SessionId('main'), provider: 'mock', model: 'mock', resumeSessionId: SessionId('does-not-exist') }] }) const warn = vi.spyOn((ctx.agentLoop as unknown as { ctx: { logger: { warn: (...a: unknown[]) => void } } }).ctx.logger, 'warn') .mockImplementation(() => undefined) diff --git a/packages/core/agent-loop/tests/contract-regressions.spec.ts b/packages/core/agent-loop/tests/contract-regressions.spec.ts index 891272a3c2..1875ce3757 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -5,7 +5,6 @@ import SessionStore, { Session, SessionEvent, SessionId, TurnEndReason } from '@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool, type PostToolDecision } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent, type ContinuationDecision } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { DEFAULT_MAX_PARALLEL_TOOL_CALLS } from '@deepseek-ai/dsh-agent-loop' import { prepareReactLoopAgent } from '../src/agent.ts' import * as Invariants from '@deepseek-ai/dsh-invariants' @@ -24,7 +23,6 @@ async function harness(adapter: MockAdapter) { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx @@ -824,7 +822,6 @@ describe('turn numbering continues across seeded sessions', () => { await ctx2.plugin(SystemPrompt) await ctx2.plugin(ToolRegistry) await ctx2.plugin(AgentRegistry) - await ctx2.plugin(AgentExecutionProvider) await ctx2.plugin(AgentLoop, { agents: [] }) ctx2.llm.registerAdapter(['mock'], second) @@ -967,7 +964,6 @@ describe('turn and step boundary recovery', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(Invariants) ctx.llm.registerAdapter(['mock'], adapter) @@ -1419,7 +1415,6 @@ describe('disposal and cancellation during pre-step assembly', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(Invariants) ctx.llm.registerAdapter(['mock'], adapter) @@ -1471,7 +1466,6 @@ describe('disposal and cancellation during pre-step assembly', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(Invariants) ctx.llm.registerAdapter(['mock'], adapter) @@ -1527,7 +1521,6 @@ describe('disposal and cancellation during pre-step assembly', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(Invariants) ctx.llm.registerAdapter(['mock'], adapter) @@ -1579,7 +1572,6 @@ describe('disposal and cancellation during pre-step assembly', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(Invariants) ctx.llm.registerAdapter(['mock'], adapter) @@ -1629,7 +1621,6 @@ describe('disposal and cancellation during pre-step assembly', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(Invariants) ctx.llm.registerAdapter(['mock'], adapter) diff --git a/packages/core/agent-loop/tests/coverage-edges.spec.ts b/packages/core/agent-loop/tests/coverage-edges.spec.ts index d7971c81bc..38fdaedf57 100644 --- a/packages/core/agent-loop/tests/coverage-edges.spec.ts +++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts @@ -6,7 +6,6 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -21,7 +20,6 @@ async function harness(adapter: MockAdapter) { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx diff --git a/packages/core/agent-loop/tests/interception.spec.ts b/packages/core/agent-loop/tests/interception.spec.ts index d3c66af720..a715265a97 100644 --- a/packages/core/agent-loop/tests/interception.spec.ts +++ b/packages/core/agent-loop/tests/interception.spec.ts @@ -5,7 +5,6 @@ import SessionStore, { SessionId, type SessionEvent, type TurnEndReason } from ' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent, type ContinuationDecision, type PromptDecision, type SessionStartSource } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -25,7 +24,6 @@ async function harness(adapter: MockAdapter) { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx diff --git a/packages/core/agent-loop/tests/loop.spec.ts b/packages/core/agent-loop/tests/loop.spec.ts index 105156f70e..63fef89087 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -5,7 +5,6 @@ import SessionStore, { SessionId, TurnEndReason } from '@deepseek-ai/dsh-session import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -20,7 +19,6 @@ async function harness(adapter: MockAdapter, persona = '') { await ctx.plugin(SystemPrompt, { persona }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx @@ -1029,7 +1027,6 @@ describe('agent loop', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [{ id: SessionId('config-agent'), provider: 'mock', model: 'mock' }], }) @@ -1054,7 +1051,6 @@ describe('agent loop', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [{ id: SessionId('config-agent'), provider: 'mock', model: 'mock', cwd: '/work/project' }], }) diff --git a/packages/core/agent-loop/tests/properties.spec.ts b/packages/core/agent-loop/tests/properties.spec.ts index 48c5d767c0..6f872cc423 100644 --- a/packages/core/agent-loop/tests/properties.spec.ts +++ b/packages/core/agent-loop/tests/properties.spec.ts @@ -18,7 +18,6 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import fc from 'fast-check' @@ -42,7 +41,6 @@ async function harness() { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], new EchoAdapter()) return ctx diff --git a/packages/core/agent-loop/tests/request-cache.e2e.ts b/packages/core/agent-loop/tests/request-cache.e2e.ts index 31c4865c61..c82eccbf85 100644 --- a/packages/core/agent-loop/tests/request-cache.e2e.ts +++ b/packages/core/agent-loop/tests/request-cache.e2e.ts @@ -5,7 +5,6 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' @@ -43,7 +42,6 @@ async function loopHarness(): Promise { await created.plugin(SystemPrompt, { persona: SYSTEM }) await created.plugin(ToolRegistry) await created.plugin(AgentRegistry) - await created.plugin(AgentExecutionProvider) await created.plugin(AgentLoop, { agents: [] }) await created.plugin(LlmDeepSeek) created.tools.register(defineTool({ diff --git a/packages/core/agent-loop/tests/request-reconstruction.spec.ts b/packages/core/agent-loop/tests/request-reconstruction.spec.ts index 20000e9057..6622092f47 100644 --- a/packages/core/agent-loop/tests/request-reconstruction.spec.ts +++ b/packages/core/agent-loop/tests/request-reconstruction.spec.ts @@ -13,7 +13,6 @@ import SessionStore, { Session, SessionId, foldRequestHeader } from '@deepseek-a import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -24,7 +23,6 @@ async function harness(adapter: MockAdapter, persona = 'stable base') { await ctx.plugin(SystemPrompt, { persona }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx diff --git a/packages/core/agent-loop/tests/resume.spec.ts b/packages/core/agent-loop/tests/resume.spec.ts index bd00eb8bdd..74192dde90 100644 --- a/packages/core/agent-loop/tests/resume.spec.ts +++ b/packages/core/agent-loop/tests/resume.spec.ts @@ -11,7 +11,6 @@ import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -31,7 +30,6 @@ async function mountPersistentHarness(root: string, adapter: MockAdapter): Promi await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SessionPersistenceJsonl, { root }) ctx.llm.registerAdapter(['mock'], adapter) @@ -139,7 +137,6 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { await ctx2.plugin(SystemPrompt) await ctx2.plugin(ToolRegistry) await ctx2.plugin(AgentRegistry) - await ctx2.plugin(AgentExecutionProvider) await ctx2.plugin(AgentLoop, { agents: [] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], adapter2) @@ -168,7 +165,6 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { await ctx2.plugin(SystemPrompt) await ctx2.plugin(ToolRegistry) await ctx2.plugin(AgentRegistry) - await ctx2.plugin(AgentExecutionProvider) await ctx2.plugin(AgentLoop, { agents: [] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], adapter2) @@ -383,7 +379,6 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) const loopFiber = await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SessionPersistenceJsonl, { root }) ctx.llm.registerAdapter(['mock'], new MockAdapter([textResponse('next')])) @@ -445,7 +440,6 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { await ctx2.plugin(SystemPrompt) await ctx2.plugin(ToolRegistry) await ctx2.plugin(AgentRegistry) - await ctx2.plugin(AgentExecutionProvider) await ctx2.plugin(AgentLoop, { agents: [] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], adapter2) @@ -499,7 +493,6 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { await ctx2.plugin(SystemPrompt) await ctx2.plugin(ToolRegistry) await ctx2.plugin(AgentRegistry) - await ctx2.plugin(AgentExecutionProvider) await ctx2.plugin(AgentLoop, { agents: [] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], adapter2) @@ -529,7 +522,6 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { await ctx2.plugin(SystemPrompt) await ctx2.plugin(ToolRegistry) await ctx2.plugin(AgentRegistry) - await ctx2.plugin(AgentExecutionProvider) await ctx2.plugin(AgentLoop, { agents: [] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], adapter2) @@ -560,7 +552,6 @@ describe('the session-persistence RFC: AgentLoop factory create/resume', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) await expect(ctx.agents.resume({ resumeSessionId: SessionId('nope') })) diff --git a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts index aba59c8a68..73ee48f7a6 100644 --- a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts +++ b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts @@ -8,7 +8,6 @@ import AgentRegistry, { agentEvents, assembleContextFor } from '@deepseek-ai/dsh import type { Agent } from '@deepseek-ai/dsh-agent' import { scopeOf } from '@deepseek-ai/dsh-scope' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -20,7 +19,6 @@ async function harnessWithLoop(adapter: MockAdapter = new MockAdapter([textRespo await ctx.plugin(SystemPrompt, { persona: 'You are the deployment.' }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) const loopFiber = await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return { ctx, loopFiber } diff --git a/packages/core/agent-loop/tests/tool-calls.spec.ts b/packages/core/agent-loop/tests/tool-calls.spec.ts index c8794d49e9..84c2fb34eb 100644 --- a/packages/core/agent-loop/tests/tool-calls.spec.ts +++ b/packages/core/agent-loop/tests/tool-calls.spec.ts @@ -11,7 +11,6 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import LlmService from '@deepseek-ai/dsh-llm' import ToolRegistry, { defineTool, type PostToolDecision, type PreToolDecision } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -22,7 +21,6 @@ async function harness(adapter: MockAdapter, maxParallelToolCalls?: number) { await ctx.plugin(SystemPrompt, { persona: '' }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [], ...maxParallelToolCalls === undefined ? {} : { maxParallelToolCalls }, @@ -346,7 +344,6 @@ describe('tool-call scheduler: rolling pool honors maxParallelToolCalls', () => await ctx.plugin(SystemPrompt, { persona: '' }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [], maxParallelToolCalls: 1 }) ctx.llm.registerAdapter(['mock'], adapter) const gated = gatedParallelTool('p') diff --git a/packages/core/agent-loop/tests/tool-order.spec.ts b/packages/core/agent-loop/tests/tool-order.spec.ts index 3346591811..a4b5f82c4a 100644 --- a/packages/core/agent-loop/tests/tool-order.spec.ts +++ b/packages/core/agent-loop/tests/tool-order.spec.ts @@ -14,7 +14,6 @@ import SystemPrompt, { TOOL_ORDER_REST } from '@deepseek-ai/dsh-system-prompt' import type { Config as SystemPromptConfig } from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -25,7 +24,6 @@ async function harness(adapter: MockAdapter, toolOrder?: SystemPromptConfig['too await ctx.plugin(SystemPrompt, { persona: 'stable base', ...toolOrder !== undefined ? { toolOrder } : {} }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx diff --git a/packages/core/agent-loop/tests/turn-stop.spec.ts b/packages/core/agent-loop/tests/turn-stop.spec.ts index 9920cb1c29..8bbd26f2ac 100644 --- a/packages/core/agent-loop/tests/turn-stop.spec.ts +++ b/packages/core/agent-loop/tests/turn-stop.spec.ts @@ -5,7 +5,6 @@ import SessionStore, { SessionId, type TurnEndReason } from '@deepseek-ai/dsh-se import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { type Agent, type ContinuationStop } from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import * as Invariants from '@deepseek-ai/dsh-invariants' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -18,7 +17,6 @@ async function harness(adapter: MockAdapter): Promise { await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(Invariants) - await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) ctx.llm.registerAdapter(['mock'], adapter) return ctx diff --git a/packages/core/agent-loop/tsconfig.json b/packages/core/agent-loop/tsconfig.json index 1e17efa41f..5d7cf98bb7 100644 --- a/packages/core/agent-loop/tsconfig.json +++ b/packages/core/agent-loop/tsconfig.json @@ -35,9 +35,6 @@ { "path": "../../core/agent" }, - { - "path": "../../core/agent-execution" - }, { "path": "../../core/scope" } diff --git a/packages/core/agent/README.md b/packages/core/agent/README.md index 122e58f39d..23d9d998c6 100644 --- a/packages/core/agent/README.md +++ b/packages/core/agent/README.md @@ -1,10 +1,10 @@ # dsh-agent -Agent interface, registry, and `agent/*` event vocabulary. Every plugin (UI, hooks, orchestrators) programs against the `Agent` handle defined here — it has zero loop dependency, so the loop is swappable. +Agent interface, registry, process-local initiator scope, and `agent/*` event vocabulary. Every plugin (UI, hooks, orchestrators) programs against the `Agent` handle defined here — it has zero loop dependency, so the loop is swappable. ## Service: `AgentRegistry` (ctx key: `agents`) -Tracks live agents so UI, hook, and orchestrator plugins can find them without importing the concrete loop package. +Tracks live agents and carries the initiating Agent through asynchronous driver work without importing the concrete loop package. ### Public API @@ -17,6 +17,17 @@ The scoped-registration surface: `Agent.ctx` is the agent's scope context (`dsh- - `ctx.agents.list(): Agent[]` - `ctx.agents.roots(): Agent[]` — live agents created without an owning agent context; a resumed lineage-bearing session can still be a runtime root. +#### Initiating Agent scope + +`AgentLoop` runs each concrete driver's complete lifetime inside an initiator boundary. Concurrent drivers remain isolated, a child driver shadows its parent, and the parent returns after the child settles. Creation, persistence load, and unpublished setup remain outside the child's boundary, so setup initiated by a parent inherits the parent while `agentCtx.agent` identifies the child explicitly. + +- `ctx.agents.currentInitiator(): Agent | undefined` — read the inherited initiator without requiring one. +- `ctx.agents.requireInitiator(): Agent` — read it or throw `no initiating agent is active`. +- `ctx.agents.withInitiator(agent, operation)` — run with one exact Agent and preserve the operation's exact synchronous value or Promise. +- `ctx.agents.withoutInitiator(operation)` — hide an inherited initiator for unrelated process-local work. + +The scope carries the `Agent` itself and is process-local. Ambient presence is neither liveness proof nor authorization; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. Teardown rejects new boundaries, lets injected dependents and returned-Promise boundaries drain, then disables the underlying `AsyncLocalStorage`; unreturned work remains owned by the subsystem that detached it. The [initiator-scope decision](../../../docs/rfc/implemented/architecture/2026-07-15-agent-initiator-scope.md) owns the detailed boundary and teardown contract. + #### Factory seam (creation) Agent *creation* is provided by the plugin implementing `AgentFactory` (`dsh-agent-loop`), registered via `setFactory`. This keeps creation on the `dsh-agent` interface so consumers (UI, the ACP bridge) program against `ctx.agents` without depending on the concrete loop package. The registry canonicalizes an already traced Service to its concrete target and re-traces each call through the caller's context; this avoids nested Cordis shadows while passing an explicit caller-bound `ownerCtx` to plain factories. @@ -72,6 +83,8 @@ The handle every plugin programs against: ## Known Limitations and Deferred Work +- **Initiator scope is process-local** — workers, child processes, HTTP, durable queues, and restarts materialize any required identity explicitly. +- **Ambient identity may outlive liveness** — consumers still check `agent.status`, cancellation, and the owning capability contract before lifecycle-sensitive work. - **Inter-agent channels beyond delegation** — shared state, streaming child output, and background/poll semantics remain outside the current synchronous `ctx.subagents` seam. - **`agent/session-start` cannot gate startup** — it remains a synchronous, veto-less notification; async composition that must finish before publication belongs in the factory's `setup(agentCtx)` transaction instead. - **No public step-only abort** — `cancel()` clears ALL pending work (queued + steering + in-flight); an abort that preserves queued prompts returns only with a named consumer ([stop-surface RFC](../../../docs/rfc/implemented/simplification/2026-06-20-public-agent-stop-surface.md)). diff --git a/packages/core/agent/package.json b/packages/core/agent/package.json index 72cd18942e..bcf75ea7ca 100644 --- a/packages/core/agent/package.json +++ b/packages/core/agent/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-agent", - "description": "Agent interface, registry, and event vocabulary for the DeepSeek Harness", + "description": "Agent interface, registry, initiator scope, and event vocabulary for the DeepSeek Harness", "version": "0.0.1", "private": true, "type": "module", diff --git a/packages/core/agent/src/index.ts b/packages/core/agent/src/index.ts index 8a9ed68ab6..f2e774223d 100644 --- a/packages/core/agent/src/index.ts +++ b/packages/core/agent/src/index.ts @@ -1,11 +1,13 @@ /** - * Agent registry service. Tracks live agents so plugins can find them without - * depending on the concrete loop package. Agent creation belongs to the loop. + * Agent service: live registry, factory delegation, and process-local + * initiator scope. Concrete creation and driving belong to the loop. * * @module @deepseek-ai/dsh-agent */ import { Context, getTraceable, Service, symbols } from 'cordis' +import { AsyncLocalStorage } from 'node:async_hooks' +import { isPromise } from 'node:util/types' import { scopeTarget } from '@deepseek-ai/dsh-scope' import type { Scoped } from '@deepseek-ai/dsh-scope' import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session' @@ -173,6 +175,8 @@ export interface AgentFactory { /** Thrown when create/resume is called before an agent factory is registered. */ const NO_FACTORY_MESSAGE = 'no agent factory registered (load an agent-loop plugin)' +const NO_INITIATOR_MESSAGE = 'no initiating agent is active' +const DISPOSED_INITIATOR_MESSAGE = 'agent initiator scope is disposed' /** All mutable lifecycle state for one exact registry entry. */ interface AgentEntry { @@ -192,15 +196,19 @@ interface FactorySlot { } /** - * Agent registry (`ctx.agents`): tracks live agents so UI, hook, and - * orchestrator plugins can find them without depending on the concrete loop - * package. Agent *creation* is provided by whichever plugin implements the - * {@link AgentFactory} (`@deepseek-ai/dsh-agent-loop`), registered via - * {@link setFactory}. + * Agent service (`ctx.agents`): tracks live agents and carries the initiating + * Agent through one process-local asynchronous driver chain. Agent *creation* + * is provided by whichever plugin implements the {@link AgentFactory} + * (`@deepseek-ai/dsh-agent-loop`), registered via {@link setFactory}. */ export class AgentRegistry extends Service { private store = new Map() private factory: FactorySlot | undefined + private readonly initiators = new AsyncLocalStorage() + private initiatorState: 'active' | 'closing' | 'disposed' = 'active' + private activeInitiatorRuns = 0 + private initiatorDrain: PromiseWithResolvers | undefined + private initiatorDisposal: Promise | undefined constructor(ctx: Context) { super(ctx, 'agents') @@ -211,6 +219,54 @@ export class AgentRegistry extends Service { // accessor body never needs to resolve a scope itself. Effect-scoped: // unwinds with this service's fiber. ctx.accessor('agent', { get: () => undefined }) + ctx.effect(function* (this: AgentRegistry) { + yield () => this.disposeInitiators() + yield () => { this.closeInitiators() } + }.bind(this), 'agents.initiatorLifecycle()') + } + + /** + * Read the Agent that initiated the inherited asynchronous driver chain. + * @returns the inherited Agent, or `undefined` outside a driver and inside an explicit clearing boundary. + * @throws when this service instance has been disposed. + */ + currentInitiator(): Agent | undefined { + this.assertInitiatorsReadable() + return this.initiators.getStore() + } + + /** + * Read the initiating Agent and fail when no driver boundary is active. + * @returns the inherited Agent. + * @throws when no initiator is active or this service instance has been disposed. + */ + requireInitiator(): Agent { + const agent = this.currentInitiator() + if (agent === undefined) throw new Error(NO_INITIATOR_MESSAGE) + return agent + } + + /** + * Run an operation with one exact Agent as its process-local initiator. The + * exact synchronous value or Promise returned by the operation is preserved. + * @param agent - initiating Agent to inherit; presence is neither liveness proof nor authorization. + * @param operation - synchronous or asynchronous operation to invoke. + * @returns the exact value returned by `operation`. + * @throws when the initiator scope is closing/disposed, or when `operation` throws. + */ + withInitiator(agent: Agent, operation: () => T): T { + return this.runWithInitiator(agent, operation) + } + + /** + * Run an operation inside a boundary that hides any inherited initiating + * Agent. The exact synchronous value or Promise is preserved. + * @param operation - synchronous or asynchronous operation to invoke without an initiator. + * @returns the exact value returned by `operation`. + * @throws when the initiator scope is closing/disposed, or when `operation` throws. + */ + withoutInitiator(operation: () => T): T { + return this.runWithInitiator(undefined, operation) } /** @@ -471,6 +527,57 @@ export class AgentRegistry extends Service { .filter(entry => entry.owner === undefined) .map(entry => entry.agent) } + + /** Reject new initiator boundaries while inherited continuations drain. */ + private closeInitiators(): void { + if (this.initiatorState === 'active') this.initiatorState = 'closing' + } + + /** Wait for returned-Promise boundaries, then invalidate retained references. */ + private disposeInitiators(): Promise { + return (this.initiatorDisposal ??= (async () => { + this.closeInitiators() + if (this.activeInitiatorRuns !== 0) { + this.initiatorDrain ??= Promise.withResolvers() + await this.initiatorDrain.promise + } + this.initiatorState = 'disposed' + this.initiators.disable() + })()) + } + + /** Establish one tracked initiator or clearing boundary. */ + private runWithInitiator(agent: Agent | undefined, operation: () => T): T { + if (this.initiatorState !== 'active') throw new Error(DISPOSED_INITIATOR_MESSAGE) + this.activeInitiatorRuns += 1 + let result: T + try { + result = this.initiators.run(agent, operation) + } catch (error: unknown) { + this.releaseInitiatorRun() + throw error + } + if (isPromise(result)) { + void result.then( + () => { this.releaseInitiatorRun() }, + () => { this.releaseInitiatorRun() }, + ) + } else { + this.releaseInitiatorRun() + } + return result + } + + private assertInitiatorsReadable(): void { + if (this.initiatorState === 'disposed') throw new Error(DISPOSED_INITIATOR_MESSAGE) + } + + private releaseInitiatorRun(): void { + this.activeInitiatorRuns -= 1 + if (this.activeInitiatorRuns !== 0) return + this.initiatorDrain?.resolve() + this.initiatorDrain = undefined + } } export default AgentRegistry diff --git a/packages/core/agent/tests/agent-initiator.spec.ts b/packages/core/agent/tests/agent-initiator.spec.ts new file mode 100644 index 0000000000..87a8387e07 --- /dev/null +++ b/packages/core/agent/tests/agent-initiator.spec.ts @@ -0,0 +1,163 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { runInNewContext } from 'node:vm' +import AgentRegistry from '@deepseek-ai/dsh-agent' +import type { Agent } from '@deepseek-ai/dsh-agent' +import { SessionId } from '@deepseek-ai/dsh-session' + +function agent(id: string): Agent { + return { id: SessionId(id) } as Agent +} + +async function harness(): Promise<{ + ctx: Context + service: AgentRegistry + dispose: () => Promise +}> { + const ctx = new Context() + const fiber = await ctx.plugin(AgentRegistry) + return { + ctx, + service: ctx.agents, + dispose: fiber.dispose, + } +} + +describe('AgentRegistry initiator scope', () => { + it('reports an absent initiator and requires an active boundary', async () => { + const { service, dispose } = await harness() + expect(service.currentInitiator()).toBeUndefined() + expect(() => service.requireInitiator()).toThrow('no initiating agent is active') + await dispose() + }) + + it('preserves exact synchronous and Promise return identities across await', async () => { + const { service, dispose } = await harness() + const initiator = agent('identity') + const value = { result: true } + expect(service.withInitiator(initiator, () => { + expect(service.requireInitiator()).toBe(initiator) + return value + })).toBe(value) + + const promise = service.withInitiator(initiator, async () => { + expect(service.requireInitiator()).toBe(initiator) + await Promise.resolve() + expect(service.requireInitiator()).toBe(initiator) + return value + }) + expect(service.withInitiator(initiator, () => promise)).toBe(promise) + await expect(promise).resolves.toBe(value) + expect(service.currentInitiator()).toBeUndefined() + await dispose() + }) + + it('isolates overlapping initiators', async () => { + const { service, dispose } = await harness() + const a = agent('a') + const b = agent('b') + const bothStarted = Promise.withResolvers() + const release = Promise.withResolvers() + let starts = 0 + const run = (initiator: Agent): Promise => service.withInitiator(initiator, async () => { + expect(service.requireInitiator()).toBe(initiator) + starts += 1 + if (starts === 2) bothStarted.resolve(true) + await release.promise + expect(service.requireInitiator()).toBe(initiator) + }) + + const pending = [run(a), run(b)] + await bothStarted.promise + expect(service.currentInitiator()).toBeUndefined() + release.resolve(true) + await Promise.all(pending) + await dispose() + }) + + it('restores nested and explicitly cleared boundaries', async () => { + const { service, dispose } = await harness() + const parent = agent('parent') + const child = agent('child') + + service.withInitiator(parent, () => { + expect(service.requireInitiator()).toBe(parent) + service.withInitiator(child, () => { expect(service.requireInitiator()).toBe(child) }) + expect(service.requireInitiator()).toBe(parent) + service.withoutInitiator(() => { + expect(service.currentInitiator()).toBeUndefined() + expect(() => service.requireInitiator()).toThrow('no initiating agent is active') + }) + expect(service.requireInitiator()).toBe(parent) + }) + expect(service.currentInitiator()).toBeUndefined() + await dispose() + }) + + it('restores the parent after synchronous throws and rejected operations', async () => { + const { service, dispose } = await harness() + const parent = agent('parent') + const child = agent('child') + const syncError = new Error('sync failure') + const asyncError = new Error('async failure') + + service.withInitiator(parent, () => { + expect(() => service.withInitiator(child, () => { throw syncError })).toThrow(syncError) + expect(service.requireInitiator()).toBe(parent) + }) + await expect(service.withInitiator(child, async () => { + await Promise.resolve() + throw asyncError + })).rejects.toBe(asyncError) + expect(service.currentInitiator()).toBeUndefined() + await dispose() + }) + + it('stops new boundaries, drains active Promises, and invalidates retained references', async () => { + const { ctx, service, dispose } = await harness() + const initiator = agent('draining') + const release = Promise.withResolvers() + const pending = service.withInitiator(initiator, async () => { + await release.promise + expect(service.requireInitiator()).toBe(initiator) + }) + let disposed = false + const disposal = dispose().then(() => { disposed = true }) + await Promise.resolve() + + expect(() => service.withInitiator(initiator, () => 1)).toThrow('agent initiator scope is disposed') + expect(() => service.withoutInitiator(() => 1)).toThrow('agent initiator scope is disposed') + expect(disposed).toBe(false) + expect(ctx.get('agents')).toBeUndefined() + release.resolve(true) + await pending + await disposal + expect(() => service.currentInitiator()).toThrow('agent initiator scope is disposed') + expect(() => service.requireInitiator()).toThrow('agent initiator scope is disposed') + }) + + it('drains cross-realm Promise boundaries before disposal', async () => { + const { service, dispose } = await harness() + const initiator = agent('cross-realm') + const release = Promise.withResolvers() + const operation = runInNewContext( + '(async () => { await release; inspect() })', + { + release: release.promise, + inspect: () => { expect(service.requireInitiator()).toBe(initiator) }, + }, + ) as () => Promise + const pending = service.withInitiator(initiator, operation) + expect(pending).not.toBeInstanceOf(Promise) + + let disposed = false + const disposal = dispose().then(() => { disposed = true }) + await Promise.resolve() + expect(disposed).toBe(false) + + release.resolve(true) + await pending + await disposal + expect(disposed).toBe(true) + }) +}) diff --git a/packages/core/agent/tests/gen-cordis-catalog.spec.ts b/packages/core/agent/tests/gen-cordis-catalog.spec.ts index 53847b0e97..24841c8c8e 100644 --- a/packages/core/agent/tests/gen-cordis-catalog.spec.ts +++ b/packages/core/agent/tests/gen-cordis-catalog.spec.ts @@ -6,7 +6,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' -import { collectEvents, collectServices } from '../../../../scripts/gen-cordis-catalog.ts' +import { collectEvents, collectServices, renderEvents, renderServices } from '../../../../scripts/gen-cordis-catalog.ts' /** Write a fixture package exposing one `interface Events` block and return the * scan root to hand `collectEvents`. */ @@ -58,6 +58,8 @@ describe('gen-cordis-catalog collectEvents', () => { )) expect(events).toHaveLength(1) expect(events[0]).toMatchObject({ name: 'fix/happened', scope: 'fix', mode: 'emit', doc: 'A thing happened.' }) + expect(events[0]?.jsDoc).toBe('/**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */') + expect(renderEvents(events)).toContain("```ts cordis-catalog\n/**\n * A thing happened.\n * @param id - which thing.\n * @mode emit\n */\n'fix/happened'(id: string): void\n```") }) it('classifies a trailing-next signature as a waterfall', () => { @@ -158,26 +160,11 @@ export class FixService { expect(services).toHaveLength(1) expect(services[0]).toMatchObject({ key: 'fix', type: 'FixService', abstract: false, doc: 'Fixture service.' }) expect(services[0]?.methods).toHaveLength(3) - }) - - it('extracts an interface service as an abstract seam', () => { - const services = collectServices(makeService(`/** Fixture service interface. */ -export interface FixService { - /** - * Do the thing. - * @param id - which thing to do. - * @returns the outcome of doing it. - */ - run(id: string): string -}`)) - expect(services).toHaveLength(1) - expect(services[0]).toMatchObject({ - key: 'fix', - type: 'FixService', - abstract: true, - doc: 'Fixture service interface.', + expect(services[0]?.methods[0]).toEqual({ + signature: 'run(id: string): string', + jsDoc: '/**\n * Do the thing.\n * @param id - which thing to do.\n * @returns the outcome of doing it.\n */', }) - expect(services[0]?.methods).toEqual(['run(id: string): string']) + expect(renderServices(services)).toContain('```ts cordis-catalog\n/**\n * Do the thing.\n * @param id - which thing to do.\n * @returns the outcome of doing it.\n */\nrun(id: string): string\n\n/** Fire and forget (void needs no @returns). */\npoke(): void') }) it('hard-errors on a public method with no JSDoc at all', () => { diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md index 794df27760..409aef5ef2 100644 --- a/packages/examples/agent-spine-demo/README.md +++ b/packages/examples/agent-spine-demo/README.md @@ -16,8 +16,7 @@ Read this package for the whole plugin tree and its composition order. @deepseek-ai/dsh-tools registry + guarded pre/around/post/final-result pipeline @deepseek-ai/dsh-skill skill provider registry @deepseek-ai/dsh-skill-local local filesystem skill provider -@deepseek-ai/dsh-agent agent registry + agent/* event vocabulary -@deepseek-ai/dsh-agent-execution process-local ambient Agent execution context +@deepseek-ai/dsh-agent agent registry + initiator scope + agent/* events @deepseek-ai/dsh-tasks generic background-task registry @deepseek-ai/dsh-invariants dev-mode event-contract assertions @deepseek-ai/dsh-tool-bash the model-facing bash schema diff --git a/packages/examples/agent-spine-demo/package.json b/packages/examples/agent-spine-demo/package.json index 7d78b5eddb..772d6c059b 100644 --- a/packages/examples/agent-spine-demo/package.json +++ b/packages/examples/agent-spine-demo/package.json @@ -1,6 +1,6 @@ { "name": "@deepseek-ai/dsh-agent-spine-demo", - "description": "The default executor-less/UI-less agent spine as one Cordis bundle plugin (timer + llm + sessions + system-prompt + tools + skills + agents + agent-execution + tasks + invariants + tool-bash + workspace-context + tool-skill + tool-tasks + agent-loop)", + "description": "The default executor-less/UI-less agent spine as one Cordis bundle plugin (timer + llm + sessions + system-prompt + tools + skills + agents + tasks + invariants + tool-bash + workspace-context + tool-skill + tool-tasks + agent-loop)", "version": "0.0.1", "private": true, "type": "module", @@ -24,7 +24,6 @@ "peerDependencies": { "@cordisjs/plugin-timer": "^1.1.2", "@deepseek-ai/dsh-agent": "^0.0.1", - "@deepseek-ai/dsh-agent-execution": "^0.0.1", "@deepseek-ai/dsh-agent-loop": "^0.0.1", "@deepseek-ai/dsh-invariants": "^0.0.1", "@deepseek-ai/dsh-home": "^0.0.1", @@ -44,7 +43,6 @@ "devDependencies": { "@cordisjs/plugin-timer": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-fs-local": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts index 338c09b586..a4965ca457 100644 --- a/packages/examples/agent-spine-demo/src/index.ts +++ b/packages/examples/agent-spine-demo/src/index.ts @@ -18,7 +18,6 @@ import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools import SkillService, { type Config as SkillRegistryConfig } from '@deepseek-ai/dsh-skill' import * as SkillLocal from '@deepseek-ai/dsh-skill-local' import AgentRegistry from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import TaskService from '@deepseek-ai/dsh-tasks' import * as invariants from '@deepseek-ai/dsh-invariants' import * as toolBash from '@deepseek-ai/dsh-tool-bash' @@ -154,7 +153,6 @@ export function apply(ctx: Context, config: Config): void { ctx.plugin(SkillService, config.skills?.registry ?? {}) ctx.plugin(SkillLocal, Object.assign({}, config.skills?.local, { dshHome })) ctx.plugin(AgentRegistry) - ctx.plugin(AgentExecutionProvider) ctx.plugin(TaskService) ctx.plugin(invariants) ctx.plugin(toolBash, Object.assign({}, config.toolBash, { dshHome })) diff --git a/packages/examples/agent-spine-demo/tsconfig.json b/packages/examples/agent-spine-demo/tsconfig.json index d1e01cf84c..89cb2accd8 100644 --- a/packages/examples/agent-spine-demo/tsconfig.json +++ b/packages/examples/agent-spine-demo/tsconfig.json @@ -41,9 +41,6 @@ { "path": "../../core/agent" }, - { - "path": "../../core/agent-execution" - }, { "path": "../../context/workspace-context" }, diff --git a/packages/sdk/helper/src/features/builtin/spine.ts b/packages/sdk/helper/src/features/builtin/spine.ts index d2fb877ec4..caf066865e 100644 --- a/packages/sdk/helper/src/features/builtin/spine.ts +++ b/packages/sdk/helper/src/features/builtin/spine.ts @@ -36,10 +36,6 @@ class SpineOption extends FeatureOption { }, ['persona'], config => requiredString(config, 'persona')), ...npmCordisConfigEntry(ID, { id: 'tools', name: '@deepseek-ai/dsh-tools' }, []), ...npmCordisConfigEntry(ID, { id: 'agent', name: '@deepseek-ai/dsh-agent' }), - ...npmCordisConfigEntry(ID, { - id: 'agent-execution', - name: '@deepseek-ai/dsh-agent-execution', - }), ...npmCordisConfigEntry(ID, { id: 'invariants', name: '@deepseek-ai/dsh-invariants' }), ...npmCordisConfigEntry(ID, { id: 'agent-loop', diff --git a/packages/support/agent-loop-testkit/README.md b/packages/support/agent-loop-testkit/README.md index cad60b21a4..350a8643e1 100644 --- a/packages/support/agent-loop-testkit/README.md +++ b/packages/support/agent-loop-testkit/README.md @@ -1,6 +1,6 @@ # `@deepseek-ai/dsh-agent-loop-testkit` -Shared prerequisite mounting for tests that exercise the concrete `AgentLoop`. `mountAgentLoopTestDependencies(ctx, options?)` installs the LLM, session, system-prompt, tool, agent, and agent-execution services in dependency order, then returns before the loop is mounted. +Shared prerequisite mounting for tests that exercise the concrete `AgentLoop`. `mountAgentLoopTestDependencies(ctx, options?)` installs the LLM, session, system-prompt, tool, and agent services in dependency order, then returns before the loop is mounted. The caller registers adapters and optional plugins, mounts `AgentLoop` with the configuration under test, and disposes its own Context. System-prompt and tool-registry configuration can be forwarded through `options`; the helper does not provide test defaults beyond those owned by the services. A plugin-load failure rejects the helper call, while services activated earlier in the sequence remain owned by the caller's Context. diff --git a/packages/support/agent-loop-testkit/package.json b/packages/support/agent-loop-testkit/package.json index 712631ab29..423bd3e80d 100644 --- a/packages/support/agent-loop-testkit/package.json +++ b/packages/support/agent-loop-testkit/package.json @@ -23,7 +23,6 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-agent": "^0.0.1", - "@deepseek-ai/dsh-agent-execution": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-system-prompt": "^0.0.1", @@ -32,7 +31,6 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", diff --git a/packages/support/agent-loop-testkit/src/index.ts b/packages/support/agent-loop-testkit/src/index.ts index 32549dbc2d..c7b0cb7304 100644 --- a/packages/support/agent-loop-testkit/src/index.ts +++ b/packages/support/agent-loop-testkit/src/index.ts @@ -7,7 +7,6 @@ import type { Context } from 'cordis' import AgentRegistry from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import LlmService from '@deepseek-ai/dsh-llm' import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' @@ -44,5 +43,4 @@ export async function mountAgentLoopTestDependencies( await ctx.plugin(SystemPrompt, options.systemPrompt ?? {}) await ctx.plugin(ToolRegistry, options.tools ?? {}) await ctx.plugin(AgentRegistry) - await ctx.plugin(AgentExecutionProvider) } diff --git a/packages/support/agent-loop-testkit/tsconfig.json b/packages/support/agent-loop-testkit/tsconfig.json index f8eddb5693..5e5b3c47f2 100644 --- a/packages/support/agent-loop-testkit/tsconfig.json +++ b/packages/support/agent-loop-testkit/tsconfig.json @@ -17,9 +17,6 @@ { "path": "../../core/agent" }, - { - "path": "../../core/agent-execution" - }, { "path": "../../llm/llm" }, diff --git a/packages/workflow/workflow-workerthread/package.json b/packages/workflow/workflow-workerthread/package.json index 7ccfbc3ebb..1cd6e07e17 100644 --- a/packages/workflow/workflow-workerthread/package.json +++ b/packages/workflow/workflow-workerthread/package.json @@ -41,7 +41,6 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", diff --git a/packages/workflow/workflow-workerthread/tests/workflow-workerthread.e2e.ts b/packages/workflow/workflow-workerthread/tests/workflow-workerthread.e2e.ts index cbde053d45..680b2e25c7 100644 --- a/packages/workflow/workflow-workerthread/tests/workflow-workerthread.e2e.ts +++ b/packages/workflow/workflow-workerthread/tests/workflow-workerthread.e2e.ts @@ -5,7 +5,6 @@ import SessionStore, { SessionId } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry from '@deepseek-ai/dsh-agent' -import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import SubagentService from '@deepseek-ai/dsh-subagent' @@ -35,7 +34,6 @@ async function harness(): Promise { await built.plugin(SystemPrompt) await built.plugin(ToolRegistry) await built.plugin(AgentRegistry) - await built.plugin(AgentExecutionProvider) await built.plugin(AgentLoop, { agents: [] }) await built.plugin(LlmDeepSeek) await built.plugin(SubagentService) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b54201c207..6c7e4a3d05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,9 +435,6 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent - '@deepseek-ai/dsh-agent-execution': - specifier: workspace:^ - version: link:../../core/agent-execution '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../../core/agent-loop @@ -533,15 +530,6 @@ importers: specifier: ^4.0.0-rc.7 version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) - packages/core/agent-execution: - devDependencies: - '@deepseek-ai/dsh-agent': - specifier: workspace:^ - version: link:../agent - cordis: - specifier: ^4.0.0-rc.7 - version: 4.0.0-rc.7(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.5) - packages/core/agent-loop: dependencies: schemastery: @@ -551,9 +539,6 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../agent - '@deepseek-ai/dsh-agent-execution': - specifier: workspace:^ - version: link:../agent-execution '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants @@ -704,9 +689,6 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent - '@deepseek-ai/dsh-agent-execution': - specifier: workspace:^ - version: link:../../core/agent-execution '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../../core/agent-loop @@ -1698,9 +1680,6 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent - '@deepseek-ai/dsh-agent-execution': - specifier: workspace:^ - version: link:../../core/agent-execution '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../../core/agent-loop @@ -2353,9 +2332,6 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../core/agent - '@deepseek-ai/dsh-agent-execution': - specifier: workspace:^ - version: link:../../core/agent-execution '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../../core/agent-loop @@ -2413,9 +2389,6 @@ importers: '@deepseek-ai/dsh-agent': specifier: workspace:^ version: link:../../packages/core/agent - '@deepseek-ai/dsh-agent-execution': - specifier: workspace:^ - version: link:../../packages/core/agent-execution '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../../packages/core/agent-loop diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index f2646835d6..2d3560ec5c 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -10,7 +10,6 @@ "@cordisjs/plugin-timer": "workspace:^", "@deepseek-ai/dsh-acp": "workspace:^", "@deepseek-ai/dsh-agent": "workspace:^", - "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-spine-demo": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-app-boot": "workspace:^", diff --git a/scripts/cordis-walk.ts b/scripts/cordis-walk.ts index 617fd34bd5..44e87b2a21 100644 --- a/scripts/cordis-walk.ts +++ b/scripts/cordis-walk.ts @@ -2,7 +2,7 @@ * Shared AST walkers for the cordis documentation generators * (`gen-cordis-catalog.ts`, `gen-website-api.ts`): locating the cordis module * merge in a source file, enumerating its `interface Events` members, and - * resolving the `interface Context` service keys to their service declarations. + * resolving the `interface Context` service keys to their service classes. * One walk, two renderers — the catalog and the website page carry different * prose but must agree on WHAT exists. */ @@ -50,50 +50,45 @@ function contextKeyMap(body: ts.ModuleBlock, sf: ts.SourceFile): Map` service declaration resolved from a Context merge. */ -export interface ServiceDeclaration { +/** One `ctx.` service class resolved from a Context merge. */ +export interface ServiceClass { key: string type: string - declaration: ts.ClassDeclaration | ts.InterfaceDeclaration + cls: ts.ClassDeclaration abstract: boolean - /** Declaration-level JSDoc prose (empty string when missing — also reported). */ + /** Class-level JSDoc prose (empty string when missing — also reported). */ doc: string } /** - * Resolve each `ctx.` of a merge to the service class or interface declared in the + * Resolve each `ctx.` of a merge to the service class declared in the * same file. A key whose type is not a class here (a Pick-mixin member, e.g. - * timer helpers) is skipped. A declaration without JSDoc prose is reported into + * timer helpers) is skipped. A class without JSDoc prose is reported into * `violations` (named `where` by the caller's gate). * * @param body — the cordis module merge body. * @param sf — the source file containing the merge. * @param rel — repo-relative path of `sf`, for violation pointers. * @param violations — sink for JSDoc-completeness violations. - * @returns the resolved service declarations, in Context-declaration order. + * @returns the resolved service classes, in Context-declaration order. */ -export function serviceDeclarations( +export function serviceClasses( body: ts.ModuleBlock, sf: ts.SourceFile, rel: string, violations: string[], -): ServiceDeclaration[] { +): ServiceClass[] { const text = sf.getFullText() - const out: ServiceDeclaration[] = [] + const out: ServiceClass[] = [] for (const [key, type] of contextKeyMap(body, sf)) { - const declaration = sf.statements.find( - (s): s is ts.ClassDeclaration | ts.InterfaceDeclaration => - (ts.isClassDeclaration(s) || ts.isInterfaceDeclaration(s)) && s.name?.text === type, + const cls = sf.statements.find( + (s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === type, ) - if (!declaration) continue // a Pick-mixin member, not a service declaration here - const abstract = ts.isInterfaceDeclaration(declaration) - || (declaration.modifiers?.some(m => m.kind === ts.SyntaxKind.AbstractKeyword) ?? false) - const doc = parseJsDoc(rawJsDoc(text, declaration)).doc - if (!doc) { - const kind = ts.isInterfaceDeclaration(declaration) ? 'interface' : 'class' - violations.push(`service ctx.${key} (${pointer(rel, sf, declaration)}): ${kind} ${type} has no JSDoc.`) - } - out.push({ key, type, declaration, abstract, doc }) + if (!cls) continue // a Pick-mixin member, not a class here + const abstract = cls.modifiers?.some(m => m.kind === ts.SyntaxKind.AbstractKeyword) ?? false + const doc = parseJsDoc(rawJsDoc(text, cls)).doc + if (!doc) violations.push(`service ctx.${key} (${pointer(rel, sf, cls)}): class ${type} has no JSDoc.`) + out.push({ key, type, cls, abstract, doc }) } return out } diff --git a/scripts/gen-cordis-api.ts b/scripts/gen-cordis-api.ts index cc87c379e9..5d8a70fba4 100644 --- a/scripts/gen-cordis-api.ts +++ b/scripts/gen-cordis-api.ts @@ -80,7 +80,7 @@ function referencedTypes(seeds: string[], decls: Map): { name: s function render(): string { const services = collectServices() const events = collectEvents().sort((a, b) => a.name.localeCompare(b.name)) - const types = referencedTypes(services.flatMap(service => service.methods), collectTypeDecls()) + const types = referencedTypes(services.flatMap(service => service.methods.map(method => method.signature)), collectTypeDecls()) const lines: string[] = [ '/**', ' * Generated by scripts/gen-cordis-api.ts — do not edit by hand; run', @@ -145,7 +145,7 @@ function render(): string { lines.push(' methods: [],') } else { lines.push(' methods: [') - for (const method of service.methods) lines.push(` ${quote(method)},`) + for (const method of service.methods) lines.push(` ${quote(method.signature)},`) lines.push(' ],') } lines.push(' },') diff --git a/scripts/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index 293215a174..08f04947da 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -9,7 +9,7 @@ import { globSync, readFileSync, writeFileSync } from 'node:fs' import { resolve, sep } from 'node:path' import ts from 'typescript' import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts' -import { cordisModuleBody, eventMembers, serviceDeclarations } from './cordis-walk.ts' +import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.ts' const root = resolve(import.meta.dirname, '..') const OUT_EVENTS = 'docs/cordis-catalog/events.md' @@ -27,8 +27,6 @@ const FENCE = 'ts cordis-catalog' // TODO(catalog-type-links): verify or generate link-map coverage. export const LINK_MAP: Record = { Agent: 'core.md', - AgentExecution: 'core.md', - AgentExecutionService: 'core.md', ContentBlock: 'core.md', Message: 'core.md', MessageSource: 'core.md', @@ -74,6 +72,8 @@ interface EventEntry { scope: string /** Full signature text (the method-signature member, JSDoc stripped). */ signature: string + /** Original declaration JSDoc, dedented from its containing interface. */ + jsDoc: string /** Dispatch mode from the `@mode` tag. */ mode: Mode /** Description prose (JSDoc minus the `@mode` tag), one line per paragraph. */ @@ -82,19 +82,27 @@ interface EventEntry { source: string } +/** One public service method and the source contract attached to it. */ +interface ServiceMethodEntry { + /** Public method signature (body stripped). */ + signature: string + /** Original method JSDoc, dedented from its containing class. */ + jsDoc: string +} + /** One harness service, extracted from an `interface Context` block. */ interface ServiceEntry { /** The `ctx.` name, e.g. `llm`. */ key: string /** The service class/interface name, e.g. `LlmService`. */ type: string - /** Whether the service declaration is abstract (a seam interface). */ + /** Whether the service class is abstract (a seam interface). */ abstract: boolean - /** Declaration-level JSDoc prose, one line per paragraph. */ + /** Class-level JSDoc prose, one line per paragraph. */ doc: string - /** Public method signatures (bodies stripped), in source order. */ - methods: string[] - /** Source pointer of the service declaration. */ + /** Public methods (bodies stripped), in source order. */ + methods: ServiceMethodEntry[] + /** Source pointer of the class declaration. */ source: string } @@ -117,6 +125,22 @@ function memberSignature(member: ts.TypeElement | ts.ClassElement, sf: ts.Source return sig.replace(/\s*;?\s*$/, '').replace(/\s+/g, ' ').trim() } +/** + * Copy a node's original JSDoc while removing only the indentation imposed by + * its containing interface or class. + */ +function jsDocText(text: string, sf: ts.SourceFile, node: ts.Node): string { + const raw = rawJsDoc(text, node) + if (!raw) return '' + const start = text.lastIndexOf(raw, node.getStart(sf)) + const { line } = sf.getLineAndCharacterOfPosition(start) + const lineStart = sf.getPositionOfLineAndCharacter(line, 0) + const indent = text.slice(lineStart, start) + return raw.split('\n') + .map((lineText, index) => index > 0 && lineText.startsWith(indent) ? lineText.slice(indent.length) : lineText) + .join('\n') +} + /** Walk every harness `interface Events` block and extract its events, hard- * erroring (aggregated) on any JSDoc-completeness violation: a missing/ * contradicted `@mode`, missing description prose, or an undocumented payload @@ -157,15 +181,15 @@ export function collectEvents(scanRoot: string = root): EventEntry[] { const { params } = parseTags(raw) checkParams(where, 'event', member.parameters, params, sf, p => (ts.isIdentifier(p.name) && p.name.text === 'this') || (hasNext && p === last), violations) - if (mode) entries.push({ name, scope: name.split('/')[0] ?? name, signature, mode, doc, source: src }) + if (mode) entries.push({ name, scope: name.split('/')[0] ?? name, signature, jsDoc: jsDocText(text, sf, member), mode, doc, source: src }) } } reportViolations('gen-cordis-catalog', violations) return entries } -/** Walk every harness `interface Context` block + its service declaration, hard- - * erroring (aggregated) on any JSDoc-completeness violation: a declaration or public +/** Walk every harness `interface Context` block + its service class, hard- + * erroring (aggregated) on any JSDoc-completeness violation: a class or public * method without JSDoc prose, an undocumented parameter, a stale `@param`, a * missing `@returns` on a non-void method, or an inferred (unannotated) return * type the pure-AST walk cannot classify. @@ -180,11 +204,11 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] { const sf = ts.createSourceFile(abs, text, ts.ScriptTarget.Latest, true) const body = cordisModuleBody(sf) if (!body) continue - // Resolve each ctx key to its service declaration (shared walk) and emit an entry. - for (const { key, type, declaration, abstract, doc: declarationDoc } of serviceDeclarations(body, sf, rel, violations)) { - const methods: string[] = [] - for (const member of declaration.members) { - if (!ts.isMethodDeclaration(member) && !ts.isMethodSignature(member)) continue + // Resolve each ctx key to its service class (shared walk) and emit an entry. + for (const { key, type, cls, abstract, doc: clsDoc } of serviceClasses(body, sf, rel, violations)) { + const methods: ServiceMethodEntry[] = [] + for (const member of cls.members) { + if (!ts.isMethodDeclaration(member)) continue // Only instance methods callable through `ctx.` are surface; // private, protected, and static methods are not. const nonPublic = member.modifiers?.some(m => @@ -195,9 +219,9 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] { if (nonPublic) continue const memberName = member.name.getText(sf) if (memberName.startsWith('[')) continue // computed/symbol members - methods.push(memberSignature(member, sf)) const where = `service method ctx.${key}.${memberName} (${pointer(rel, sf, member)})` const raw = rawJsDoc(text, member) + methods.push({ signature: memberSignature(member, sf), jsDoc: jsDocText(text, sf, member) }) if (!raw) { violations.push(`${where} has no JSDoc.`); continue } if (!parseJsDoc(raw).doc) violations.push(`${where} has no description prose above its block tags.`) const { params, returns } = parseTags(raw) @@ -212,9 +236,9 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] { key, type, abstract, - doc: declarationDoc, + doc: clsDoc, methods, - source: pointer(rel, sf, declaration), + source: pointer(rel, sf, cls), }) } } @@ -277,7 +301,7 @@ function typeLinks(signature: string): string { function renderEvent(e: EventEntry): string[] { const out = [`### \`${e.name}\` — ${e.mode}`, ''] if (e.doc) out.push(e.doc, '') - out.push('```' + FENCE, e.signature, '```', '') + out.push('```' + FENCE, e.jsDoc, e.signature, '```', '') const links = typeLinks(e.signature) if (links) out.push(links, '') out.push(`Source: [\`${e.source}\`](../../${e.source.split(':')[0]})`, '') @@ -290,8 +314,13 @@ function renderService(s: ServiceEntry): string[] { const out = [`## \`ctx.${s.key}\` — \`${s.type}\`${kind}`, ''] if (s.doc) out.push(s.doc, '') if (s.methods.length) { - out.push('```' + FENCE, ...s.methods, '```', '') - const links = typeLinks(s.methods.join('\n')) + const declarations = s.methods.flatMap((method, index) => [ + ...(index > 0 ? [''] : []), + method.jsDoc, + method.signature, + ]) + out.push('```' + FENCE, ...declarations, '```', '') + const links = typeLinks(s.methods.map(method => method.signature).join('\n')) if (links) out.push(links, '') } out.push(`Source: [\`${s.source}\`](../../${s.source.split(':')[0]})`, '') @@ -306,15 +335,15 @@ const BANNER = [ ] /** The shared GENERATED + freshness-gate + fence notice paragraph. */ -const GATE_NOTICE = 'This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence (skipped by doc-typecheck, since a bare signature is not standalone-compilable). Type names in a signature link to the page that documents them.' +const GATE_NOTICE = 'This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verified fresh by `pnpm run verify-cordis-catalog` (part of `doc-sync`) — do not edit it by hand. Signature blocks use a `ts cordis-catalog` fence and include the original source JSDoc immediately before each event or service method. doc-typecheck skips these bare declaration fragments; type names in a signature link to the page that documents them.' /** Render the events catalog (pure, deterministic given sorted inputs). */ -function renderEvents(events: EventEntry[]): string { +export function renderEvents(events: EventEntry[]): string { const lines: string[] = [ ...BANNER, '# Cordis Events Catalog', '', - 'Every cordis event a plugin can listen to: exact signature, dispatch mode, and the declaration\'s JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around.', + 'Every cordis event a plugin can listen to: exact signature, dispatch mode, and original declaration JSDoc. This is one axis of the **wiring** reference a plugin author works against — the callable `ctx.` surface is the sibling [services catalog](services.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around.', '', GATE_NOTICE, '', @@ -344,12 +373,12 @@ function renderEvents(events: EventEntry[]): string { } /** Render the services catalog (pure, deterministic given sorted inputs). */ -function renderServices(services: ServiceEntry[]): string { +export function renderServices(services: ServiceEntry[]): string { const lines: string[] = [ ...BANNER, '# Cordis Services Catalog', '', - 'Every `ctx.` service a plugin can call: the exact public interface plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.', + 'Every `ctx.` service a plugin can call: the exact public interface with original method JSDoc, plus the class JSDoc. This is one axis of the **wiring** reference a plugin author works against — the events a plugin listens to are the sibling [events catalog](events.md), and [core-data-structures/](../core-data-structures/core.md) catalogs the *data structures* these signatures move around. An abstract seam (e.g. `ctx.bash`) is implemented by a separate package; the interface is what consumers code against.', '', GATE_NOTICE, '', diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 94836eaefd..19a124ccdf 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -156,18 +156,10 @@ const SERVICE_ROLES: ServiceRole[] = [ { key: 'agents', pkg: 'agent', - title: 'Agent registry', + title: 'Agent service', mode: 'core', consumers: ['agent-loop', 'acp', 'subagent-inprocess', 'stdio-demo', 'invariants'], - note: 'Owns live Agent handles and the create/resume factory seam.', - }, - { - key: 'agentExecution', - pkg: 'agent-execution', - title: 'Agent execution context', - mode: 'core', - consumers: ['agent-loop'], - note: 'Carries the exact initiating Agent across one process-local asynchronous driver chain; explicit identities remain authoritative at external boundaries.', + note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.', }, { key: 'agentLoop', diff --git a/scripts/gen-website-api.ts b/scripts/gen-website-api.ts index bae24c3f3d..0420ad2ff3 100644 --- a/scripts/gen-website-api.ts +++ b/scripts/gen-website-api.ts @@ -35,7 +35,7 @@ import { globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import ts from 'typescript' import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc, reportViolations, type Mode } from './jsdoc.ts' -import { cordisModuleBody, eventMembers, serviceDeclarations } from './cordis-walk.ts' +import { cordisModuleBody, eventMembers, serviceClasses } from './cordis-walk.ts' const root = resolve(import.meta.dirname, '..') @@ -197,18 +197,6 @@ function isPublicInstance(member: ts.ClassElement): boolean { return !member.name.getText().startsWith('_') } -type HarnessServiceMember = ts.MethodDeclaration | ts.MethodSignature | ts.PropertyDeclaration - | ts.PropertySignature | ts.GetAccessorDeclaration - -/** Whether a class/interface service member is renderable public API. */ -function isPublicServiceMember(member: HarnessServiceMember): boolean { - if (ts.isMethodSignature(member) || ts.isPropertySignature(member)) { - if (ts.isComputedPropertyName(member.name)) return false - return !member.name.getText().startsWith('_') - } - return isPublicInstance(member) -} - /** Whether a class member is renderable public STATIC API. */ function isPublicStatic(member: ts.ClassElement): boolean { const mods = ts.getCombinedModifierFlags(member) @@ -465,15 +453,14 @@ function collectHarnessServices(violations: string[]): HarnessService[] { // Manifest shape is repo-owned; `name` is the one field read here. const manifest = JSON.parse(readFileSync(pkgJson, 'utf8')) as { name: string } const pkg = manifest.name - for (const { key, type, declaration, abstract, doc: declarationDoc } of serviceDeclarations(body, sf, rel, violations)) { - const groups = new Map() - for (const member of declaration.members) { + for (const { key, type, cls, abstract, doc: clsDoc } of serviceClasses(body, sf, rel, violations)) { + const groups = new Map() + for (const member of cls.members) { // Public properties are API too: ctx.codeRuntime.language/isolation // are readonly descriptors consumers key presentation off. - const renderable = ts.isMethodDeclaration(member) || ts.isMethodSignature(member) - || ts.isPropertyDeclaration(member) || ts.isPropertySignature(member) || ts.isGetAccessorDeclaration(member) + const renderable = ts.isMethodDeclaration(member) || ts.isPropertyDeclaration(member) || ts.isGetAccessorDeclaration(member) if (!renderable) continue - if (!isPublicServiceMember(member)) continue + if (!isPublicInstance(member)) continue const name = member.name.getText(sf) const group = groups.get(name) ?? [] group.push(member) @@ -481,7 +468,7 @@ function collectHarnessServices(violations: string[]): HarnessService[] { } const members = [...groups.entries()].map(([name, group]) => memberDoc(`ctx.${key}.${name} (${rel})`, name, group, rel, violations)) - services.push({ key, type, abstract, doc: declarationDoc, members, source: pointer(rel, sf, declaration), pkg }) + services.push({ key, type, abstract, doc: clsDoc, members, source: pointer(rel, sf, cls), pkg }) } } return services.sort((a, b) => a.key.localeCompare(b.key)) diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index b6a92c55f6..566bdade7b 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -15,8 +15,6 @@ { "doc": "docs/core-data-structures/core.md", "symbol": "SessionEvent", "source": "packages/core/session/src/types.ts" }, { "doc": "docs/core-data-structures/core.md", "symbol": "InjectOptions", "source": "packages/core/agent/src/types.ts" }, { "doc": "docs/core-data-structures/core.md", "symbol": "Agent", "source": "packages/core/agent/src/types.ts" }, - { "doc": "docs/core-data-structures/core.md", "symbol": "AgentExecution", "source": "packages/core/agent-execution/src/types.ts" }, - { "doc": "docs/core-data-structures/core.md", "symbol": "AgentExecutionService", "source": "packages/core/agent-execution/src/index.ts" }, { "doc": "docs/core-data-structures/core.md", "symbol": "HookContext", "source": "packages/core/agent/src/types.ts" }, { "doc": "docs/core-data-structures/core.md", "symbol": "PromptDecision", "source": "packages/core/agent/src/types.ts" }, { "doc": "docs/core-data-structures/core.md", "symbol": "ContinuationDecision", "source": "packages/core/agent/src/types.ts" }, diff --git a/scripts/verify-package-readme-model-experience.ts b/scripts/verify-package-readme-model-experience.ts index f42c346678..35cd2dae1b 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -28,7 +28,6 @@ interface SentenceContract { * so an absent section cannot be mistaken for forgotten documentation. */ const NO_MODEL_EXPERIENCE_SECTION: Readonly> = { - 'packages/core/agent-execution': 'The package adds no model-visible text or schema; consumers own any use in model requests.', 'packages/core/scope': 'The package is a model-agnostic registration and lifecycle primitive; model-facing consumers own any context selection.', 'packages/util/brand': 'The package is a type-only primitive erased at compile time.', 'packages/util/paths': 'The package only resolves harness-owned host paths; model-facing consumers own any rendered use.', diff --git a/tsconfig.build.json b/tsconfig.build.json index c631871672..31a42172e4 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -25,7 +25,6 @@ { "path": "./packages/session-query/session-query" }, { "path": "./packages/core/system-prompt" }, { "path": "./packages/core/agent" }, - { "path": "./packages/core/agent-execution" }, { "path": "./packages/context/time-context" }, { "path": "./packages/ui/user-interaction" }, { "path": "./packages/ui/user-approval" }, diff --git a/tsconfig.json b/tsconfig.json index 8c8a2531c8..9d1a299b1b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,7 +36,6 @@ { "path": "./packages/session-query/session-query" }, { "path": "./packages/core/system-prompt" }, { "path": "./packages/core/agent" }, - { "path": "./packages/core/agent-execution" }, { "path": "./packages/context/time-context" }, { "path": "./packages/ui/user-interaction" }, { "path": "./packages/ui/user-approval" }, diff --git a/website/.vitepress/config/api-sidebar.json b/website/.vitepress/config/api-sidebar.json index c0774e23a4..3a2b11db3c 100644 --- a/website/.vitepress/config/api-sidebar.json +++ b/website/.vitepress/config/api-sidebar.json @@ -22,10 +22,6 @@ } ], "harness": [ - { - "text": "ctx.agentExecution", - "link": "/zh-CN/api/harness/agent-execution" - }, { "text": "ctx.agentLoop", "link": "/zh-CN/api/harness/agent-loop" diff --git a/website/zh-CN/api/harness/agent-execution.md b/website/zh-CN/api/harness/agent-execution.md deleted file mode 100644 index 2276d8ecbb..0000000000 --- a/website/zh-CN/api/harness/agent-execution.md +++ /dev/null @@ -1,48 +0,0 @@ - - -# ctx.agentExecution - -`AgentExecutionService` (abstract seam) — provided by `@deepseek-ai/dsh-agent-execution`. - -Ambient Agent identity within one process-local asynchronous chain. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-execution/src/index.ts#L18) - -### ctx.agentExecution.current() - -```ts website-api -current(): AgentExecution | undefined -``` - -Read the active execution without requiring one. - -**Returns** the inherited execution, or `undefined` outside/inside a cleared boundary. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-execution/src/index.ts#L24) - -### ctx.agentExecution.require() - -```ts website-api -require(): AgentExecution -``` - -Read the active execution and fail when no boundary is active. - -**Returns** the inherited execution. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-execution/src/index.ts#L31) - -### ctx.agentExecution.run(execution, operation) - -```ts website-api -run(execution: AgentExecution | undefined, operation: () => T): T -``` - -Run an operation inside an execution boundary. Passing `undefined` clears an inherited execution; the exact synchronous value or Promise is returned. - -- `execution` — execution to inherit, or `undefined` for a clearing boundary. -- `operation` — synchronous or asynchronous operation to invoke. - -**Returns** the exact value returned by `operation`. - -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-execution/src/index.ts#L41) diff --git a/website/zh-CN/api/harness/agent-loop.md b/website/zh-CN/api/harness/agent-loop.md index 79003deb84..6401898e24 100644 --- a/website/zh-CN/api/harness/agent-loop.md +++ b/website/zh-CN/api/harness/agent-loop.md @@ -6,7 +6,7 @@ Concrete agent factory and driver service. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L408) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L407) ### ctx.agentLoop.create(id, options?, meta?) @@ -22,7 +22,7 @@ Create an agent and session under one caller-supplied identity, owned by the acc **Returns** the published running agent. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L543) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L542) ### ctx.agentLoop.createAgent(ownerCtx, options) @@ -37,7 +37,7 @@ Create an owned agent on a caller-supplied session id. **Returns** the published handle. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L565) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L564) ### ctx.agentLoop.resume(ownerCtx, options) @@ -52,4 +52,4 @@ Resume an owned agent from the configured persistence service. **Returns** the published handle. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L597) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L596) diff --git a/website/zh-CN/api/harness/agents.md b/website/zh-CN/api/harness/agents.md index 8d6c84cc7a..328ea6097e 100644 --- a/website/zh-CN/api/harness/agents.md +++ b/website/zh-CN/api/harness/agents.md @@ -4,9 +4,62 @@ `AgentRegistry` — provided by `@deepseek-ai/dsh-agent`. -Agent registry (`ctx.agents`): tracks live agents so UI, hook, and orchestrator plugins can find them without depending on the concrete loop package. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory. +Agent service (`ctx.agents`): tracks live agents and carries the initiating Agent through one process-local asynchronous driver chain. Agent *creation* is provided by whichever plugin implements the AgentFactory (`@deepseek-ai/dsh-agent-loop`), registered via setFactory. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L201) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L204) + +### ctx.agents.currentInitiator() + +```ts website-api +currentInitiator(): Agent | undefined +``` + +Read the Agent that initiated the inherited asynchronous driver chain. + +**Returns** the inherited Agent, or `undefined` outside a driver and inside an explicit clearing boundary. + +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L233) + +### ctx.agents.requireInitiator() + +```ts website-api +requireInitiator(): Agent +``` + +Read the initiating Agent and fail when no driver boundary is active. + +**Returns** the inherited Agent. + +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L243) + +### ctx.agents.withInitiator(agent, operation) + +```ts website-api +withInitiator(agent: Agent, operation: () => T): T +``` + +Run an operation with one exact Agent as its process-local initiator. The exact synchronous value or Promise returned by the operation is preserved. + +- `agent` — initiating Agent to inherit; presence is neither liveness proof nor authorization. +- `operation` — synchronous or asynchronous operation to invoke. + +**Returns** the exact value returned by `operation`. + +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L257) + +### ctx.agents.withoutInitiator(operation) + +```ts website-api +withoutInitiator(operation: () => T): T +``` + +Run an operation inside a boundary that hides any inherited initiating Agent. The exact synchronous value or Promise is preserved. + +- `operation` — synchronous or asynchronous operation to invoke without an initiator. + +**Returns** the exact value returned by `operation`. + +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L268) ### ctx.agents.setFactory(factory) @@ -20,7 +73,7 @@ Register the agent-creation factory (the loop calls this on construction, effect **Returns** the disposer that clears the factory slot. The exact Cordis effect disposer (single-shot): composite (generator) effects may yield it directly — exact identity nests the teardown in order. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L228) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L284) ### ctx.agents.create(options) @@ -34,7 +87,7 @@ Create and publish a new agent through the registered factory. Distinct from reg **Returns** the handle after setup, rollback-covered publication, and loop start complete. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L261) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L317) ### ctx.agents.resume(options) @@ -48,7 +101,7 @@ Load a persisted session and resume an agent on it through the registered factor **Returns** the handle after setup, rollback-covered publication, and loop start complete. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L280) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L336) ### ctx.agents.register(agent) @@ -62,7 +115,7 @@ Register a live agent. Throws if an agent with the same id is already registered **Returns** the EXACT Cordis effect disposer (single-shot; a repeat call returns undefined without awaiting an in-flight teardown). Exact identity is load-bearing: a composite (generator) effect that owns a teardown ORDER — the agent factory's lifecycle chain — must yield THIS function so Cordis nests the unregistration at that yield position; yielding a wrapper would leave it disposing as a concurrent sibling on owner unload, unregistering the agent (and emitting `agent/disposed`) while its final turn is still draining. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L306) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L362) ### ctx.agents.enter(agent, owner) @@ -77,7 +130,7 @@ Insert an already-constructed agent without announcing it. This is the advanced **Returns** an idempotent closure that removes this exact entry and emits `agent/disposed` with listener failures contained. When called from a synchronous `agent/created` listener, removal and disposal wait until that creation dispatch unwinds. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L330) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L386) ### ctx.agents.announce(agent) @@ -89,7 +142,7 @@ Announce an agent previously inserted with enter. - `agent` — the live inserted agent to announce. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L405) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L461) ### ctx.agents.get(id) @@ -103,7 +156,7 @@ Look up a live agent. **Returns** the agent, or undefined when no live agent has that id. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L439) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L495) ### ctx.agents.isOwnedBy(id, owner) @@ -118,7 +171,7 @@ Test whether a live agent was created through one exact parent agent's scoped co **Returns** true only while the exact child entry is live under that owner. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L451) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L507) ### ctx.agents.list() @@ -130,7 +183,7 @@ All live agents, in registration order. **Returns** a fresh array; mutating it does not affect the registry. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L459) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L515) ### ctx.agents.roots() @@ -142,4 +195,4 @@ All live top-level agents in registration order. A top-level agent was created w **Returns** a fresh array; mutating it does not affect the registry. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L469) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent/src/index.ts#L525) diff --git a/website/zh-CN/api/harness/events.md b/website/zh-CN/api/harness/events.md index 66bec18578..8d494739ba 100644 --- a/website/zh-CN/api/harness/events.md +++ b/website/zh-CN/api/harness/events.md @@ -228,7 +228,7 @@ A declarative agent entry failed before it could publish a live agent. Consumers - `sessionId` — exact shared agent/session identity that failed startup. - `error` — persistence, setup, or publication failure. -[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L363) +[Source](https://github.com/deepseek-harness/deepseek-harness/blob/master/packages/core/agent-loop/src/index.ts#L362) ## approval/*