diff --git a/docs/architecture.md b/docs/architecture.md index 848980a45a..d4faf07f4b 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -4,7 +4,7 @@ The **DeepSeek Harness SDK** builds agent harnesses on Cordis. The principle is ## Overview -A harness is one [Cordis](cordis-primer.md) context. Packages contribute service keys, typed events, and disposable registrations: services expose stable calls (`ctx.llm`, `ctx.tools`, `ctx.sessions`), events provide interception and notifications (`agent/request`, `tools/pre-execute`, `session/event`), and registrations install prompt sections, tools, providers, adapters, or listeners. +A harness is one [Cordis](cordis-primer.md) context whose plugins contribute services, typed events, and disposable registrations. `packages/core/` groups the default agent flow; surrounding capabilities are equally first-class Cordis plugins. @@ -17,6 +17,7 @@ A harness is one [Cordis](cordis-primer.md) context. Packages contribute service | `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.agentLoop` | `dsh-agent-loop` | shipped `ReactLoopAgent` driver | ### Capability Services @@ -43,9 +44,9 @@ Events form the service extension API; see the exhaustive [events catalog](cordi ### Event Domains -- **Session events** are durable, replayable facts. Turn and step boundaries, user input, assistant output, tool calls, tool results, steering, compaction records, and tool-owned durable facts append to the session log and flow through `session/event`. -- **Agent events** carry the live `Agent` handle for status, diagnostics, prompt admission, call-config shaping, result validation, and continuation policy. -- **Capability events** belong to the seam that owns the action. `tools/*`, `llm/*`, `system-prompt/*`, `fs/*`, and `subagent/*` let policy and adapters attach without importing the loop. +- **Session events** are durable facts: turn/step boundaries, model input/output, tool activity, steering, compaction, and tool-owned records append to the log and flow through `session/event`. +- **Agent events** carry the live `Agent` handle through request and lifecycle policy. +- **Capability events** belong to the owning seam; policy and adapters attach without importing the loop. ### Interception Semantics @@ -53,7 +54,7 @@ Waterfall events behave like around-middleware: a listener delegates by calling ## Default Loop Lifecycle -The shipped loop drains work, assembles requests, streams model answers, executes tools, applies continuation policy, and checkpoints state. Every pause is a service call or event available to plugins. +The shipped loop drains work, assembles and streams requests, executes tools, applies continuation policy, and checkpoints through plugin-visible services and events. A **session** is one agent's append-only event log. A **turn** drains one queued batch and runs until the model stops asking for tools and no plugin requests continuation. A **step** is one model request plus the tool executions caused by that response. In the flow below ([sequence companion](agent-lifecycle.md)), quoted names are durable session events and event names are extension points. @@ -97,13 +98,13 @@ forever: The loop renders one prompt assembly per step. Plugins contribute ordered sections, tool schemas, and `{{name}}` variables; unknown or valueless references fail the turn instead of shipping a hole. `dsh-system-prompt` owns the harness identity and default deployment persona; an agent-scoped persona may shadow the default. The loop supplies `model` and `cwd`. See the [prompt-ownership RFC](rfc/implemented/architecture/2026-07-05-prompt-variables-and-tool-guidance-ownership.md). -Post-tool context lands after all tool results so tool-call/result adjacency stays stable. Steering drains between steps; ordinary leftover steering after a turn is re-queued as input. A terminal `agent/turn-stop` is the explicit exception: it runs after ordinary continuation and steering folding, then remains authoritative through turn close and flush so steering from those later listeners is discarded rather than becoming another step or turn; ordinary queued prompts are preserved. +Post-tool context follows all results, preserving call/result adjacency. Steering drains between steps and otherwise requeues after a turn. A terminal `agent/turn-stop` remains authoritative through turn close and flush, discarding later steering but preserving queued prompts. ### Failure Boundaries -The turn is the containment boundary. A throwing listener, adapter error finish, or failed step ends the current turn with an error reason and reports live diagnostics through `agent/error`; it does not kill the driver loop. `cancel()` clears queued and steering work, aborts the active model/tool boundary when possible, and records the appropriate turn end. Disposal stops the loop, awaits quiescence, unregisters the agent, and lets service disposers drain. +The turn contains listener, adapter, and step failures: it records an error reason and emits `agent/error` without killing the driver. `cancel()` clears pending work, aborts active model/tool work when possible, and records the turn end. Disposal stops and drains the loop before unregistering the agent. -Every session event is turn-enclosed. Reloading a crashed session preserves the interrupted tail and closes it with a synthetic `interrupted` turn end. A failure after the durable turn has closed reports through `agent/error` only because no safe in-turn position remains. A turn ends with one `TurnEndReason` (`completed`, `aborted`, `error`, `disposed`, `max-tokens`, `rejected`, or `interrupted`); per-variant semantics are in [session.md § TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap). +Every session event is turn-enclosed. Reload closes an interrupted tail with a synthetic `interrupted` end; failures after durable turn close only emit `agent/error`. A turn has one `TurnEndReason`; [TurnEndReasonMap](core-data-structures/session.md#why-a-turn-ended-turnendreasonmap) defines each variant. ### Agent Handles @@ -111,7 +112,11 @@ Every session event is turn-enclosed. Reloading a crashed session preserves the ### Agent Scope -Every live agent owns a scoped `agent.ctx`. Its registrations shadow same-named globals, receive only that agent's dispatches, and unwind with the agent; async effects such as background-task cleanup are awaited. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. The [semantic-gates RFC](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md) defines typed resolvers that derive carrier checks from merged `Events` signatures and `scopeTarget`, eliminating the handwritten event table. See the [agent-scope RFC](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md); subagent composition controls are documented [separately](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md). +Every live agent owns a scoped `agent.ctx`; its registrations shadow globals, receive only that agent's dispatches, and unwind with awaited cleanup. `CreateAgentOptions.setup(agentCtx)` composes the scope before publication. See the [scope](rfc/implemented/architecture/2026-07-08-agent-scope-contexts.md), [typed carrier checks](rfc/implemented/process/2026-07-14-typescript-program-backed-semantic-gates.md), and [subagent composition](rfc/implemented/feature/2026-07-12-subagent-persona-tool-filter-and-depth.md) decisions. + +### Agent Execution Context + +`AgentLoop` wraps each concrete driver in process-local `ctx.agentExecution`; child creation and setup stay outside its boundary, and explicit identities remain authoritative. See the [package contract](../packages/core/agent-execution/README.md) and [decision](rfc/implemented/architecture/2026-07-15-agent-execution-context.md). ## State diff --git a/docs/capability-seams.md b/docs/capability-seams.md index af4beab19e..cee9d6ed17 100644 --- a/docs/capability-seams.md +++ b/docs/capability-seams.md @@ -45,6 +45,8 @@ flowchart LR 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_agentLoop["ctx.agentLoop
Concrete loop driver"] pkg_agent_spine_demo["agent-spine-demo"] pkg_bash["bash"] @@ -91,6 +93,7 @@ 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 @@ -133,6 +136,7 @@ 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 @@ -198,6 +202,7 @@ flowchart LR | `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.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.sandbox` | `seam` | [`sandbox`](../packages/sandbox/sandbox) | [`sandbox-local`](../packages/sandbox/sandbox-local) | [`bash-sandbox`](../packages/bash/bash-sandbox) | - | Consumers hand over the exact argv they are about to spawn; same-world backends wrap it under a per-call policy and report enforcement. | diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 91fe7c6497..06547594ec 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -65,7 +65,7 @@ Source: [`packages/examples/acp-demo/src/index.ts:31`](../packages/examples/acp- ## `@deepseek-ai/dsh-agent-loop` -Requires: `agents` · `sessions` · `llm` · `tools` · `systemPrompt` +Requires: `agents` · `agentExecution` · `sessions` · `llm` · `tools` · `systemPrompt` ```ts config-catalog /** Plugin configuration for declarative startup agents. */ @@ -84,7 +84,7 @@ export interface Config { Depends on: [`AgentId`](../packages/core/agent/src/index.ts) · [`AgentOptions`](../packages/core/agent/src/index.ts) · [`SessionId`](../packages/core/session/src/index.ts) -Source: [`packages/core/agent-loop/src/index.ts:322`](../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:323`](../packages/core/agent-loop/src/index.ts) ## `@deepseek-ai/dsh-agent-spine-demo` @@ -134,7 +134,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) -Source: [`packages/examples/agent-spine-demo/src/index.ts:55`](../packages/examples/agent-spine-demo/src/index.ts) +Source: [`packages/examples/agent-spine-demo/src/index.ts:56`](../packages/examples/agent-spine-demo/src/index.ts) ## `@deepseek-ai/dsh-bash-local` @@ -1250,6 +1250,7 @@ Source: [`packages/workflow/workflow-workerthread/src/index.ts:32`](../packages/ 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/services.md b/docs/cordis-catalog/services.md index 18e00b7904..dc4976238f 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -9,6 +9,20 @@ This file is GENERATED from source (`scripts/gen-cordis-catalog.ts`) and verifie 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:17`](../../packages/core/agent-execution/src/index.ts) + ## `ctx.agentLoop` — `AgentLoop` Concrete ReactLoopAgent factory and driver service. @@ -19,7 +33,7 @@ async createAgent(ownerCtx: Context, options: CreateAgentOptions): Promise ``` -Source: [`packages/core/agent-loop/src/index.ts:335`](../../packages/core/agent-loop/src/index.ts) +Source: [`packages/core/agent-loop/src/index.ts:336`](../../packages/core/agent-loop/src/index.ts) ## `ctx.agents` — `AgentRegistry` diff --git a/docs/core-data-structures/core.md b/docs/core-data-structures/core.md index 340212d37a..133458f55b 100644 --- a/docs/core-data-structures/core.md +++ b/docs/core-data-structures/core.md @@ -351,6 +351,50 @@ 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 + +`AgentExecution` is the process-local ambient frame established around a concrete driver's lifetime. It holds the exact Agent 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 +} +``` + ## Interception decisions Each `agent/*` interception waterfall returns a small, seam-specific typed union — the unified Decision idiom (the tool seams' `PreToolDecision`/`PostToolDecision` in [tools.md](tools.md) follow the same shape). A CC/Codex hook bridge maps its `permissionDecision`/`decision`/`continue`/`additionalContext` fields onto these; a native plugin returns them directly. They share one envelope for model-facing context, `HookContext`, which is `inject()`ed as a `context/message` and so carries a REQUIRED `source` (a missing source would default to `{kind:'user'}` and mislabel plugin context as a user prompt). diff --git a/docs/module-graph.md b/docs/module-graph.md index 82763bdc55..3cd212c61d 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -18,6 +18,7 @@ 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"] @@ -175,6 +176,7 @@ 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 @@ -230,6 +232,7 @@ flowchart TD pkg_stdio --> pkg_session pkg_stdio --> pkg_user_interaction 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 @@ -330,6 +333,7 @@ flowchart TD pkg_jsonrpc --> pkg_session pkg_jsonrpc --> pkg_subagent 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_invariants pkg_agent_spine_demo --> pkg_llm @@ -409,6 +413,7 @@ 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) | | [`hook-protocol`](../packages/hooks/hook-protocol) | `hooks` | [`bash`](../packages/bash/bash), [`session`](../packages/core/session) | @@ -425,7 +430,7 @@ flowchart TD | [`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) | | [`stdio`](../packages/ui/stdio) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`user-interaction`](../packages/ui/user-interaction) | -| [`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) | +| [`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) | | [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`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-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | @@ -447,7 +452,7 @@ flowchart TD | [`hooks-claude`](../packages/hooks/hooks-claude) | `hooks` | [`agent`](../packages/core/agent), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | | [`subagent-mock`](../packages/support/subagent-mock) | `support` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent) | | [`jsonrpc`](../packages/ui/jsonrpc) | `ui` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`llm-deepseek`](../packages/llm/llm-deepseek), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent) | -| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools) | +| [`agent-spine-demo`](../packages/examples/agent-spine-demo) | `examples` | [`agent`](../packages/core/agent), [`agent-execution`](../packages/core/agent-execution), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tasks`](../packages/tasks/tasks), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/skill/tool-skill), [`tool-tasks`](../packages/tasks/tool-tasks), [`tools`](../packages/core/tools) | | [`workflow-workerthread`](../packages/workflow/workflow-workerthread) | `workflow` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | | [`subagent-fork`](../packages/subagent/subagent-fork) | `subagent` | [`agent`](../packages/core/agent), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | | [`subagent-spawn`](../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../packages/subagent/subagent), [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | diff --git a/docs/rfc/INDEX.md b/docs/rfc/INDEX.md index 2660132fb2..bc355fa328 100644 --- a/docs/rfc/INDEX.md +++ b/docs/rfc/INDEX.md @@ -28,7 +28,6 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | Title | First proposed | |---|---| | [Runtime schemas for the event vocabulary (Zod vs the merge-extensible-map pattern)](proposed/architecture/2026-06-16-typed-event-schemas.md) | 2026-06-16 | -| [Agent execution context over AsyncLocalStorage](proposed/architecture/2026-07-15-agent-execution-context.md) | 2026-07-15 | | [SDK project editing architecture](proposed/architecture/2026-07-15-sdk-project-editing-architecture.md) | 2026-07-15 | ### Process @@ -150,6 +149,7 @@ Generated by `pnpm run gen-rfc-index` from the RFC tree — never edit by hand; | [The agent is a registration scope](implemented/architecture/2026-07-08-agent-scope-contexts.md) | 2026-07-08 | | [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 | +| [Agent execution context over AsyncLocalStorage](implemented/architecture/2026-07-15-agent-execution-context.md) | 2026-07-15 | ### Process diff --git a/docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.i18n.yaml b/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.i18n.yaml similarity index 64% rename from docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.i18n.yaml rename to docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.i18n.yaml index 65be95551b..24dfc87f70 100644 --- a/docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.i18n.yaml +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.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: 7bea10fb1268c91a7668f7d269f83ae1250c373c -2026-07-15-agent-execution-context.zh.md: 5a7b12974818a076434ff1a1b3b4ab819866b3d7 +2026-07-15-agent-execution-context.md: 9f41aee74dbd94fa5acf93bface57618c604ec17 +2026-07-15-agent-execution-context.zh.md: 4747a506b4fb8a0ff798043ec772a4e810f84d7f 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 new file mode 100644 index 0000000000..9f41aee74d --- /dev/null +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.md @@ -0,0 +1,71 @@ +# 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 still needs a trusted initiating Agent. Capability transports, tracing helpers, loggers, and gateway clients may sit below the explicit loop, tool, and request parameters. Threading `agent` through every private helper adds plumbing, while a process-global mutable slot is incorrect across `await`. Model-visible arguments are also unsuitable because a model must not choose a trusted Session or routing header. + +## Decision + +`@deepseek-ai/dsh-agent-execution` provides the mandatory `ctx.agentExecution` service using Node `AsyncLocalStorage`. The frame contains only the exact live Agent: + +```text +export interface AgentExecution { + readonly agent: Agent +} + +export interface AgentExecutionService { + current(): AgentExecution | undefined + require(): AgentExecution + run(execution: AgentExecution | undefined, operation: () => T): T +} +``` + +`current()` is optional, `require()` throws `no agent execution context is active`, and `run()` preserves the operation's exact synchronous value or Promise. `run(undefined, operation)` establishes a real 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 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. + +Asynchronous resources created inside `run()` inherit its store even when the returned operation does not await them. Agent-owned foreground work may inherit `{ agent }` but keeps the explicit cancellation and disposal contract of its execution seam. Unrelated timers, queues, and deployment infrastructure start under `run(undefined, operation)` and own an explicit stop. 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, synchronous and awaited propagation, overlapping and nested boundaries, explicit clearing, restoration after throw or rejection, exact return identity, drain ordering, and disposed-reference errors. AgentLoop integration tests run overlapping real drivers, nested parent/child creation, agentless direct tool execution, cancellation during provider/root teardown, service restart, and a captured Agent after disposal. + +The test-double capability transport derives `X-Harness-Session-Id` internally and asserts that neither its tool schema nor logged arguments contains an identity field. Composition tests and generated catalogs keep the provider present in the default bundle, SDK spine, Python runtime closure, and direct AgentLoop harnesses; a missing provider leaves AgentLoop inactive. + +## 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 plumbing 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. The wrapper leaves room for a separately justified stale-safe label without flattening the store to a bare Agent. + +**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 live capability 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 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 new file mode 100644 index 0000000000..4747a506b4 --- /dev/null +++ b/docs/rfc/implemented/architecture/2026-07-15-agent-execution-context.zh.md @@ -0,0 +1,71 @@ +# 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。能力传输层、追踪辅助函数、日志器和网关客户端可能位于显式 loop、工具及请求参数的下层。在每个私有辅助函数中传递 `agent` 会增加管道代码,而进程级可变槽会在 `await` 之间发生并发错误。模型可见参数同样不合适,因为模型不能选择可信的会话或路由请求头。 + +## 决策 + +`@deepseek-ai/dsh-agent-execution` 使用 Node `AsyncLocalStorage` 提供必载的 `ctx.agentExecution` 服务。该帧只包含准确的存活 Agent: + +```text +export interface AgentExecution { + readonly agent: Agent +} + +export interface AgentExecutionService { + current(): AgentExecution | undefined + require(): AgentExecution + run(execution: AgentExecution | undefined, operation: () => T): T +} +``` + +`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()` 内创建的异步资源会继承其存储,即使返回的操作没有等待它们。Agent 所拥有的前台工作可以继承 `{ agent }`,但仍使用其执行 seam 的显式取消和 dispose 契约。无关的定时器、队列和部署基础设施在 `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` 的静态含义。 + +## 验证 + +服务测试锁定可选与必需读取、同步与跨 `await` 传播、并发与嵌套边界、显式清空、throw 或 rejection 后的恢复、准确返回值身份、排空顺序及已 dispose 引用错误。AgentLoop 集成测试覆盖重叠的真实驱动、嵌套父子创建、无 Agent 的直接工具执行、提供方或根 Context teardown 期间的取消、服务重启,以及 Agent dispose 后保留的引用。 + +测试替身能力传输层在内部推导 `X-Harness-Session-Id`,并断言工具 schema 与记录的参数都不包含身份字段。组合测试和生成目录确保默认 bundle、SDK 主干、Python 运行时闭包及直接 AgentLoop harness 都装载提供方;缺少提供方时 AgentLoop 保持未激活。 + +## 考虑过的替代方案 + +**在每个函数中传递 Agent。** 公开、worker、进程、持久化和协议边界继续显式传递,但要求每个进程内私有辅助函数都携带 Agent 只会增加管道代码,不会提高可信度。ALS 仅限于这些显式边界内部的异步调用链。 + +**让 `ctx.agent` 变成动态值。** `ctx.agent` 已经表示与 Agent 作用域 Cordis 上下文静态关联的 Agent。改变根上下文的含义会混合注册作用域与执行作用域,并让并发行为变得意外。 + +**保存完整的可变运行时帧。** Agent、会话、inbox、取消、轮次、步骤、工具执行和持久化已经有各自的真源。重复保存会产生陈旧快照和另一套生命周期。包装对象为另行论证的陈旧安全标签保留扩展空间,而不会把存储简化成裸 Agent。 + +**包含步骤级 `AbortSignal`、cwd、沙箱或授权。** 它们的生命周期与权限不匹配驱动边界,而且现有 seam 已经显式传递这些值。新增控制能力需要独立决策和嵌套生命周期契约。 + +**使用进程级 `currentAgent`。** 并发 Agent 和 subagent 会在异步 continuation 间相互覆盖,因此可变全局值只在 Harness 不具备的串行保证下才正确。 + +**从模型可见参数推导身份。** 不能信任模型或用户输入来选择会话、租户或沙箱路由。 + +**给每个能力 seam 增加路由身份。** 这会把宿主关注点扩散到提供方无关 API。宿主感知实现拥有其传输请求头,而公开边界继续显式传递身份。 + +## 后果 + +深层基础设施可以获得一个可信的进程内发起 Agent,而无需加宽现有工具和能力请求。并发及嵌套驱动会自动隔离,缺少提供方时 AgentLoop 保持未激活,HMR 或根 Context dispose 会在禁用 ALS 前达到静止状态。 + +该依赖不会出现在函数签名中,并且携带一个存活能力对象。消费方必须将其限制在横切基础设施中,把隐式存在视为既不证明存活、也不授予权限,并保留显式取消和归属检查。ALS 还有常驻传播成本,也无法跨越 worker、进程、HTTP 或持久化队列边界。 + +该帧有意省略轮次、步骤、signal、cwd、沙箱和授权。若真实消费方无法使用现有显式字段,必须另行论证扩展;陈旧字段最多只能误标遥测数据,绝不能授予控制权。 diff --git a/docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.md b/docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.md deleted file mode 100644 index 7bea10fb12..0000000000 --- a/docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.md +++ /dev/null @@ -1,207 +0,0 @@ -# RFC: Agent execution context over AsyncLocalStorage - -Status: proposed - -English | [中文](2026-07-15-agent-execution-context.zh.md) - -## Problem - -The harness has two useful but different notions of context: - -- A Cordis `Context` is a composition and lifetime object. The deployment context exposes shared services, while `agent.ctx` exposes the flat registration layer owned by one live Agent. -- Agent, Session, turn, step, and tool identity are execution subjects. The loop passes them explicitly through events, prompt assembly, LLM requests, and `ToolExecution`. - -These concepts must not be conflated. In particular, `agent.ctx.agent` is a static association on the Agent's scoped composition context. A plain root context deliberately returns `undefined`; it cannot be changed to mean "whichever Agent happens to be running now" because one Node process may run many Agents concurrently. - -This leaves a practical gap for deeply nested infrastructure. A capability transport, skill provider, tracing helper, logger, or gateway client may need to know which Agent initiated the current asynchronous operation. Passing `agent` through every intermediate helper is noisy, while deriving identity from a process-global mutable slot is incorrect as soon as two Agents overlap. Model-visible tool arguments are also the wrong carrier: the model must not be able to choose a trusted Session or sandbox-routing header. - -The gap becomes important when a single Harness runtime multiplexes Sessions for a multi-tenant hosting platform. Outbound capability requests must automatically carry the current Harness Session ID so the host can resolve the correct tenant and sandbox owner. Model-facing skills and tools should not know host-specific routing, but the selected capability implementation still needs a trusted current Agent at the transport boundary. - -## Proposal - -Add a narrow Agent execution-context facility backed by Node `AsyncLocalStorage`. It provides ambient access to the Agent associated with the current asynchronous execution chain without replacing Cordis contexts, explicit protocol fields, or durable Session state. - -The first version stores only the Agent: - -```text -export interface AgentExecution { - readonly agent: Agent -} - -export interface AgentExecutionService { - current(): AgentExecution | undefined - require(): AgentExecution - run(execution: AgentExecution | undefined, operation: () => T): T -} -``` - -`Session` is derived as `execution.agent.session`; it is not duplicated in the store. Turn, step, tool call, model, cwd, and sandbox identity remain outside the first version because they already have authoritative owners and no confirmed ambient consumer requires them yet. The single-field wrapper is deliberate: a later execution-frame refinement extends `AgentExecution` without changing `run()` callers, so implementations must not flatten the store to a bare `Agent`. - -`AgentExecution` deliberately retains the exact live `Agent`, not an id snapshot. This is the one capability admitted to the first-version store because it is the subject whose driver establishes the boundary and because existing scoped helpers operate on that exact object. Ambient presence is not proof of liveness or authorization: consumers must still honor the Agent lifecycle and the explicit capability contract before performing lifecycle-sensitive work. - -The API must always establish an ALS boundary, including when the supplied execution is `undefined`. This provides an explicit way to clear inherited context for unrelated detached work. A comparable implementation observed an uncleared ambient value crossing scheduled work into a later turn; the explicit undefined boundary prevents that class of leak. - -### Package and service placement - -Create `packages/core/agent-execution/` as `@deepseek-ai/dsh-agent-execution`. The package owns the Node-specific ALS implementation and augments Cordis with the mandatory `ctx.agentExecution` service. It belongs to `core/` because it is part of the stable Agent control spine that every concrete Agent loop and ambient-identity consumer programs against. - -The public key is `ctx.agentExecution`, settled here so every surface — service key, interface name, and package name — shares one word root. It names the Agent-owned asynchronous chain rather than one turn or tool call. `ctx.execution` is too broad; a runtime-flavored name would collide with `packages/code-runtime/` and with "Harness runtime" meaning the whole process; and changing `ctx.agent` is excluded because it already means the static Agent association of `agent.ctx`. - -The package exposes the service through Cordis rather than a mutable module-global slot: - -- the Agent Loop can inject the service explicitly; -- tests can mount an isolated service per Harness context; -- service disposal can disable its ALS instance after dependent Agent drivers quiesce; -- the dependency remains visible in Cordis configuration and generated catalogs. - -The service loads mandatorily with the standard agent composition bundle, and `dsh-agent-loop` declares it in `inject`: a composition that drives agents without it fails at load, per the fail-loud rule, rather than degrading to absent ambient identity at the first deep consumer. Configuration tests pin this policy. The facility relies only on stable Node `AsyncLocalStorage`, available without a polyfill across the supported `node ^22.19 || >=24` range. Node 24+ uses an `AsyncContextFrame`-backed implementation, while Node 22 uses the earlier implementation; this RFC accepts the always-on propagation cost for the invariant and makes no zero-overhead claim. - -Service teardown is ordered rather than transparent. The Agent Loop stops accepting new work, cancels and drains every driver, and only then may the service disable its ALS instance. HMR of the service rebuilds that dependent subtree; it does not preserve an in-flight turn across reload. A retained reference to a disposed service throws a stable disposed-service error from both `current()` and `require()` instead of silently returning `undefined`. - -### Lifecycle boundary - -Bind the execution context around each concrete Agent driver's `runLoop` lifetime: - -```text -agentExecution.run({ agent }, () => runLoop(ctx, agent, handle)) -``` - -This gives every operation initiated by that driver the same trusted Agent: - -- prompt interception and prompt assembly; -- LLM adapter calls; -- tool policy and tool bodies; -- capability providers and transports; -- synchronous and asynchronous helpers awaited by those operations. - -Concurrent drivers receive distinct ALS stores. A child Agent's own driver establishes a new boundary with the child, so child operations do not inherit the parent Agent merely because child creation started inside a parent tool call. When a nested boundary returns, ALS restores the parent automatically. - -Agent creation setup is deliberately outside this dynamic boundary. Setup already receives `agentCtx`, whose `agentCtx.agent` is the correct unpublished Agent. Publication and lifecycle ownership continue to use the existing explicit Agent and scoped carrier. One consequence is a defined contract, not an accident: when child creation starts inside a parent tool call, the child's setup and persistence load run under the PARENT's ambient identity, because the child's driver has not started. A transport reached during that window routes under the parent's Session — correct for trusted routing, since the parent initiated and owns the creation work. Setup code that needs the child's identity uses the explicit `agentCtx.agent`, never the ambient store. - -### Explicit subjects remain authoritative - -Ambient identity is a convenience for deep infrastructure, not a replacement for existing contracts: - -- `AgentEventDispatch` continues to carry the explicit Agent subject and scope. -- `AssembleContext.agent` remains explicit. -- `ToolExecution.agent` remains explicit and continues to select the scoped tool and policy view. -- `GenerateOptions.sessionId` remains explicit at the LLM boundary. -- Subagent requests and lifecycle events continue to carry explicit parent and child identity. -- Session events remain the durable truth for replay and resume. - -Code at a public service, process, worker, persistence, or wire boundary must materialize the identity it needs into that boundary's typed request. A remote process cannot access the parent's ALS store. - -### Trusted transport use - -A host-aware capability transport may read `ctx.agentExecution.require().agent.session.id` when constructing an outbound request and add a deployment-owned trusted header such as `X-Harness-Session-Id`. The header is not present in model-visible tool arguments and cannot be overridden by the model. Ambient presence alone does not authorize a request; the transport still runs inside its normal explicit capability and Agent-lifecycle contracts. - -The bash seam's existing `OwnerToken` is the nearest explicit-identity precedent and shows why it does not close this gap: `BashExecSpec.owner` is a background-task isolation key that `dsh-tool-bash` casts from the session id, foreground `run()` deliberately ignores it, and the filesystem seam has no counterpart — its provider methods carry no identity parameter at all. Extending every capability seam with a routing-identity parameter would push hosting concerns into seam vocabularies that are otherwise deployment-neutral; ambient identity lets the transport implementation own routing without widening any seam. - -The hosting platform remains responsible for resolving the Harness runtime Session ID to its product Session and sandbox owner. Harness does not learn the host's sandbox identifier, sandbox provider, or persistence model. - -Model-facing skill and tool plugins should not add hosting-specific headers themselves. They call a capability service; the selected provider owns remote execution and identity propagation. This preserves the separation between model behavior and backend routing. - -### Detached asynchronous work - -Node ALS is inherited by asynchronous resources created inside `run()`, even when callers do not await them. This is useful for an Agent-owned background operation, but it can also retain a stale turn's context in unrelated work. - -Identity inheritance does not replace cancellation ownership. Work started inside an Agent's boundary is either **foreground** — it inherits `{ agent }` and separately receives the explicit cancellation signal owned by its execution seam — or **detached** — it starts under `run(undefined, operation)` and owns its own lifecycle with an explicit stop. The caller must keep those choices aligned. The implementation must document and test these rules: - -- Work logically owned by the Agent is foreground: it may inherit `{ agent }`, receives cancellation through the existing explicit seam, and must honor the Agent's disposal contract. -- Long-lived deployment infrastructure, timers, and work queues unrelated to that Agent are detached: they must start under `run(undefined, operation)` and be stopped by their own owner, never implicitly by a turn ending. -- Code that enqueues data for later processing must serialize the required identity into the queue item; it must not expect ALS to cross the queue, process, or worker boundary. -- Consumers must not treat an ambient Agent reference as proof that the Agent is still live. Lifecycle-sensitive operations still check `agent.status`, an explicit signal, or the owning service's contract. - -`turn` and `step` remain outside the first version; they can join later as a separate immutable execution-frame refinement if a real cross-cutting consumer (tracing, logging) cannot use the existing explicit fields. The full `Agent` is the deliberate capability exception because it is the execution subject that establishes the boundary. Every additional field must be a stale-safe label whose stale copy can at worst mislabel a trace; another capability or control channel requires its own RFC. `AbortSignal` is excluded from the first version under that rule; see Alternatives considered. - -## Current Harness evidence - -The implementation Session should re-check these symbols on its target branch before editing because this handoff was prepared against a local source snapshot and the branch may have advanced. - -- `packages/core/agent/src/types.ts`: `Agent` already owns `session`, `status`, and `ctx`. Its `ctx` documentation defines a registration scope, not a dynamic request context. -- `packages/core/agent/src/index.ts`: Cordis `Context.agent` is installed as an Agent-scope DX association and defaults to `undefined` on a plain context. Do not change this semantic. -- `packages/core/agent-loop/src/agent.ts`: `ReactLoopAgent` already owns inbox, cancellation, per-step abort, status, and driver lifetime. Do not create a parallel mutable runtime-state object. -- `packages/core/agent-loop/src/loop.ts`: `runLoop(ctx, agent, handle)` has the exact lifetime boundary to wrap. It passes Agent, turn, step, and signal explicitly to narrower operations. -- `packages/core/tools/src/index.ts`: `ToolExecutionInput.agent` is explicit and selects scoped policy and tool resolution. It remains in the contract after ALS is added. -- `packages/core/agent/src/dispatch.ts`: `agentEvents()` deliberately fuses the Agent subject with its scoped carrier. Ambient context must not replace this correctness mechanism. -- `packages/core/README.md` and the existing core packages: they show that stable Agent control contracts belong in `core/`; `agent-execution` is mandatory control infrastructure rather than optional model-visible context enrichment. - -This proposal extends, rather than supersedes, [the Agent registration-scope decision](../../implemented/architecture/2026-07-08-agent-scope-contexts.md) and its [runtime design](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md). - -## Claude Code reference implementation - -| Claude Code | Harness translation | -|---|---| -| AppState store | Cordis deployment services and their owned live state | -| QueryEngine | `ReactLoopAgent` plus its loop-owned runtime state | -| ToolUseContext | Explicit Agent/tool/request parameters at capability seams | -| AgentContext ALS | Proposed narrow `AgentExecution` carrier | -| Transcript | Event-sourced `Session` and persistence backends | - -## Implementation handoff - -The implementation Session should perform the work in this order: - -1. Switch to the intended target branch and inspect the current versions of the files listed under "Current Harness evidence". Do not merge or copy changes from the branch on which this handoff was authored. -2. Add `packages/core/agent-execution/` with package metadata, README, exported types, the Cordis service, module augmentation, and focused tests. -3. Add the package to TypeScript project references, path candidates, runtime closure/configuration, and generated catalogs according to existing package gates. Prefer repository generators over hand-editing generated files. Also update the `core/` repository-layout line in root `AGENTS.md`, the package table in `packages/core/README.md`, and the package-group description in `packages/README.md`. -4. Make the Agent Loop declare and consume the service. Wrap each Agent driver's complete `runLoop` invocation in `{ agent }` without changing public Agent, event, tool, LLM, or Session signatures. -5. Add an integration test that overlaps two Agents in one process and observes the correct ambient Agent from inside asynchronous tool execution after at least one `await`. -6. Add nested-Agent coverage proving a child sees itself and the parent context is restored after the child boundary settles. -7. Add clearing and failure coverage: outside a boundary returns `undefined`, `require()` fails clearly, `run(undefined, ...)` masks an inherited Agent, and thrown/rejected operations do not contaminate later unrelated work. -8. Add a test-double capability transport to the integration suite. Keep the model-facing schema unchanged and assert that a trusted Session header is generated internally. Adapting a production remote backend is follow-up work outside this RFC. -9. Run typecheck, targeted tests, documentation gates, generated-catalog checks, and then the repository's normal CI/pre-push gate. - -Suggested focused test matrix: - -| Scenario | Required observation | -|---|---| -| Outside driver | `current()` is `undefined` | -| One Agent across awaits | Every continuation sees the same exact Agent | -| Two concurrent Agents | A never observes B and B never observes A | -| Nested child | Child sees child; parent is restored afterward | -| Child creation window | Setup inside a parent tool call sees the parent ambiently; `agentCtx.agent` is the child | -| Direct Agent-less tool call | Explicit tool behavior remains valid; ambient identity is absent | -| Cleared detached work | `run(undefined, ...)` hides the inherited Agent | -| Failure and cancellation | Context restores after throw, rejection, and abort | -| Agent disposal | Lifecycle-sensitive consumers reject work from a captured Agent after disposal | -| Service reload | Agent drivers drain before ALS disable; retained disposed-service calls throw the documented stable error | -| Capability transport boundary | Session identity is materialized into the typed request/header by the test-double transport | - -## Alternatives considered - -**Pass Agent through every function.** This remains the right choice at public and authority-bearing boundaries, but forcing it through every private helper creates plumbing that ambient execution context is designed to remove. The proposal keeps explicit subjects at seams and uses ALS only within one trusted asynchronous process. - -**Change `ctx.agent` to return the currently executing Agent.** Rejected because `ctx.agent` already denotes the static association of an Agent-scoped Cordis context. Making a root context dynamic would combine registration scope with execution scope, produce surprising behavior under concurrency, and break the implemented Agent-scope RFCs. - -**Store a complete mutable runtime object in ALS.** Rejected because Agent, Session, inbox, cancellation, turn/step state, tool execution, and durable log already have authoritative owners. Duplicating them creates stale snapshots, write-order questions, and another lifecycle to clean up. - -**Carry the step `AbortSignal` in the first-version ALS frame.** Rejected for this RFC. The signal is per-step while the proposed boundary is per-driver, so carrying it requires nested step and tool boundaries plus explicit rules for detached work, deadline ownership, and restoration. Existing execution seams already pass cancellation explicitly. A future RFC may revisit this only with a concrete cross-cutting consumer and tests that establish those nested lifecycle semantics. - -**Use one process-global mutable `currentAgent`.** Rejected because concurrent Agents and subagents overwrite one another across awaits. It is correct only under serialization, which multi-Agent execution explicitly does not guarantee. - -**Infer the Session from model-visible tool arguments.** Rejected because the model can alter those arguments. Sandbox routing and authorization require a trusted in-process identity, not user/model input. - -**Put a hosting platform's sandbox-owner identifier or provider data in Harness context.** Rejected because sandbox ownership is hosting-product state resolved outside Harness. Harness should carry only its own Session identity across the trusted transport boundary. - -## Acceptance criteria - -- One Node Harness process can execute at least two Agents concurrently, and asynchronous consumers always observe the exact initiating Agent. -- Outside Agent driver execution, ambient lookup returns `undefined` and `require()` throws a stable, actionable error. -- Nested Agent execution restores the parent context after the child settles. -- `agent.ctx`, `ctx.agent`, Agent events, prompt assembly, `ToolExecution.agent`, LLM `sessionId`, and Session persistence retain their existing semantics. -- No Agent, Session, turn, step, sandbox, or authorization identity becomes model-controlled. -- The implementation provides an explicit undefined boundary for unrelated detached work and tests it against context leakage without changing existing explicit cancellation contracts. -- The service loads with the standard agent bundle and `dsh-agent-loop` fails at load without it; a configuration test pins the policy. -- Disposal/HMR drains every dependent Agent driver before disabling ALS; retained calls on the disposed service fail with the documented stable error, and no active ALS state remains reachable through the disposed Cordis context. -- A test-double capability transport proves trusted Session ID propagation without adding a model-visible schema field. -- Package catalogs, dependency graphs, API docs, and relevant architecture docs are regenerated or updated, and the repository's documentation gates pass. - -## Risks - -- Ambient context hides a dependency from function signatures. Restricting it to deep cross-cutting infrastructure and retaining explicit public subjects limits that cost. -- ALS inheritance into detached promises and timers can retain semantically stale identity. An explicit undefined boundary, documentation, and regression tests are required rather than assumed cleanup. -- ALS does not cross worker threads, subprocesses, Redis, HTTP, or persisted queues. Every such boundary must serialize the required identity explicitly. -- The ambient store intentionally carries the full live Agent capability. A captured reference can outlive publication, so ambient presence alone never authorizes lifecycle-sensitive work and consumers must still honor Agent lifecycle and cancellation contracts. -- Mandatory loading adds a core runtime dependency to every agent composition; the RFC accepts that cost because an optional service would make ambient identity composition-dependent. Propagation cost remains measurable across supported Node versions and should be benchmarked separately. -- Adding turn, step, signal, cwd, or tool details prematurely would expand inheritance and staleness hazards. The first version deliberately accepts the limitation of Agent-only ambient identity; any additional capability or control field requires a separate RFC. diff --git a/docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.zh.md b/docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.zh.md deleted file mode 100644 index 5a7b129748..0000000000 --- a/docs/rfc/proposed/architecture/2026-07-15-agent-execution-context.zh.md +++ /dev/null @@ -1,207 +0,0 @@ -# RFC:基于 AsyncLocalStorage 的 agent(智能体)执行上下文 - -Status: proposed - -[English](2026-07-15-agent-execution-context.md) | 中文 - -## 问题 - -harness 中存在两种有用但含义不同的上下文: - -- Cordis `Context` 是依赖组合和生命周期对象。部署上下文暴露共享服务,`agent.ctx` 则暴露某个存活 Agent 所拥有的扁平注册层。 -- Agent、会话、轮次、步骤和工具身份是执行主体。agent loop(智能体循环)通过事件、提示词组装、LLM(大语言模型)请求和 `ToolExecution` 显式传递这些信息。 - -这两类概念不能混为一谈。尤其是,`agent.ctx.agent` 是 Agent 作用域组合上下文上的静态关联。普通根上下文会有意返回 `undefined`;不能把它改成“当前恰好正在运行的 Agent”,因为一个 Node 进程可能并发运行多个 Agent。 - -这给深层基础设施留下了一个实际缺口。能力传输层、skill(技能)提供方、追踪辅助函数、日志记录器或网关客户端,可能需要知道当前异步操作由哪个 Agent 发起。让每一层中间辅助函数都继续传递 `agent` 会产生大量样板代码,而从进程级可变全局槽推导身份,会在两个 Agent 并发后立即出错。模型可见的工具参数也不是合适的载体:模型不能选择可信的会话或沙箱路由请求头。 - -当单个 Harness 运行时为多租户宿主平台复用多个会话时,这个缺口会变得尤其重要。对外能力请求必须自动携带当前 Harness 会话 ID,以便宿主平台解析正确的租户和沙箱归属。模型侧的 skill 和工具不应理解宿主平台特有的路由,但所选能力实现仍需要在传输边界获得可信的当前 Agent。 - -## 提案 - -新增一套由 Node `AsyncLocalStorage` 支撑的窄粒度 Agent 执行上下文能力。它允许代码在当前异步执行链内访问关联的 Agent,但不会取代 Cordis 上下文、显式协议字段或持久化会话状态。 - -第一版只保存 Agent: - -```text -export interface AgentExecution { - readonly agent: Agent -} - -export interface AgentExecutionService { - current(): AgentExecution | undefined - require(): AgentExecution - run(execution: AgentExecution | undefined, operation: () => T): T -} -``` - -`Session` 通过 `execution.agent.session` 推导,不在存储中重复保存。轮次、步骤、工具调用、模型、cwd 和沙箱身份不进入第一版,因为它们已经有各自的真源,而且目前没有已确认的隐式上下文消费方需要这些信息。单字段包装是有意为之:后续的执行帧扩展可以在不改动 `run()` 调用方的前提下扩展 `AgentExecution`,因此实现不得把存储简化成裸 `Agent`。 - -`AgentExecution` 有意保留准确的存活 `Agent`,而不是 ID 快照。这是第一版存储中唯一获准的能力对象,因为它正是由驱动建立边界的执行主体,而且现有作用域辅助函数依赖这个准确对象。隐式存在不代表仍然存活或已经获得授权:消费方执行生命周期敏感工作前,仍须遵循 Agent 生命周期和显式能力契约。 - -API 必须始终建立 ALS 边界,即使传入的 execution 是 `undefined` 也不例外。这样可以显式清除无关分离任务继承到的上下文。一个同类实现曾观察到未清空的隐式值穿过已调度工作泄漏进后续轮次;显式 undefined 边界可以防止这类泄漏。 - -### 包与服务位置 - -在 `packages/core/agent-execution/` 新建 `@deepseek-ai/dsh-agent-execution`。该包拥有 Node 专用的 ALS 实现,并通过必载的 `ctx.agentExecution` 服务扩展 Cordis。它属于 `core/`,因为这是每个具体 Agent loop 和隐式身份消费方所依赖的稳定 Agent 控制主干。 - -公开键名在此定为 `ctx.agentExecution`,服务键、接口名和包名共用同一个词根。它表示某个 Agent 所拥有的异步调用链,而不是单个轮次、步骤或工具调用;名字也直接说明存储的内容。`ctx.execution` 含义过宽;带 runtime 字样的名字会与 `packages/code-runtime/` 以及指整个进程的 “Harness 运行时” 冲突;修改 `ctx.agent` 被排除,因为它已经表示 `agent.ctx` 与 Agent 之间的静态关联。 - -该包通过 Cordis 暴露服务,而不是使用可变模块全局槽: - -- Agent Loop 可以显式注入该服务; -- 测试可以为每个 Harness 上下文挂载隔离的服务; -- 服务 dispose(资源释放)时可以在依赖它的 Agent 驱动静止后禁用其 ALS 实例; -- 依赖关系在 Cordis 配置和生成目录中保持可见。 - -该服务随标准 agent 组合包强制加载,`dsh-agent-loop` 在 `inject` 中声明它:缺少该服务的 agent 组合按快速失败规则在加载时报错,而不是等到第一个深层消费方读取时才发现隐式身份缺失。配置测试锁定这一策略。该能力只依赖稳定的 Node `AsyncLocalStorage`,支持范围 `node ^22.19 || >=24` 全部可原生使用且无需 polyfill。Node 24 及以上使用基于 `AsyncContextFrame` 的实现,Node 22 使用此前的实现;本 RFC 为保证该不变量接受常驻传播成本,不作零开销承诺。 - -服务关闭是有顺序的,不提供透明的进行中延续。Agent Loop 必须先停止接受新驱动并取消或等待所有进行中的驱动收敛,随后 Cordis 才 dispose 服务并调用 `disable()`。HMR(热模块替换)会重建依赖该服务的子树,不承诺让进行中的轮次跨服务替换继续执行。如果旧调用方保留了已 dispose 的服务引用,`current()` 和 `require()` 都会抛出稳定的 “service disposed” 错误,而不是返回模糊的 `undefined`。 - -### 生命周期边界 - -在每个具体 Agent 驱动的 `runLoop` 整个生命周期外围绑定执行上下文: - -```text -agentExecution.run({ agent }, () => runLoop(ctx, agent, handle)) -``` - -这样,由该驱动发起的每项操作都能获得同一个可信 Agent: - -- 提示词拦截和提示词组装; -- LLM 适配器调用; -- 工具策略和工具主体; -- 能力提供方和传输层; -- 这些操作所等待的同步和异步辅助函数。 - -并发驱动会获得彼此独立的 ALS 存储。子 Agent 自己的驱动会用该子 Agent 建立新边界,因此即使子 Agent 是在父 Agent 的工具调用中创建的,其操作也不会错误继承父 Agent。嵌套边界返回后,ALS 会自动恢复父 Agent。 - -Agent 创建阶段有意置于这个动态边界之外。创建过程已经接收 `agentCtx`,其中 `agentCtx.agent` 就是正确的、尚未发布的 Agent。发布流程和生命周期归属继续使用现有的显式 Agent 与作用域载体。由此产生一条明确契约,而非偶然行为:当子 Agent 的创建发生在父 Agent 的工具调用内时,子 Agent 的创建流程和持久化加载运行在**父 Agent** 的隐式身份之下,因为子驱动尚未启动。这个窗口内触达的传输层按父会话路由——对可信路由而言这是正确的,因为创建工作由父 Agent 发起并归它所有。创建代码需要子身份时使用显式的 `agentCtx.agent`,绝不读隐式存储。 - -### 显式主体仍是真源 - -隐式身份只是深层基础设施的便利能力,不会取代现有契约: - -- `AgentEventDispatch` 继续携带显式 Agent 主体和作用域。 -- `AssembleContext.agent` 保持显式传递。 -- `ToolExecution.agent` 保持显式传递,并继续选择作用域内的工具和策略视图。 -- `GenerateOptions.sessionId` 在 LLM 边界上保持显式传递。 -- subagent 请求和生命周期事件继续携带显式的父子身份。 -- 会话事件仍然是回放和恢复的持久化真源。 - -代码跨越公开服务、进程、worker、持久化或协议边界时,必须把边界所需身份写入其类型化请求。远程进程无法访问父进程的 ALS 存储。 - -### 可信传输层用途 - -能力传输层可以在构造对外请求时读取 `ctx.agentExecution.require().agent.session.id`,并添加由部署方控制的可信身份,例如 `X-Harness-Session-Id` 请求头。该身份不出现在模型可见的参数中,模型也不能覆盖它。传输层仍须执行自身的能力和生命周期授权;隐式 Agent 只提供发起方身份,不授予调用权限。 - -bash seam 现有的 `OwnerToken` 是最接近的显式身份先例,它也说明了为什么显式方案补不上这个缺口:`BashExecSpec.owner` 是一个后台任务隔离键,由 `dsh-tool-bash` 从会话 id 转换而来,前台 `run()` 有意忽略它,而文件系统 seam 没有对应物——其提供方方法完全不携带身份参数。给每个能力 seam 都加一个路由身份参数,会把宿主平台的关注点塞进本应与部署无关的 seam 词汇;隐式身份让传输层实现自己拥有路由逻辑,而不必加宽任何 seam。 - -宿主平台继续负责把 Harness 运行时会话 ID 解析成产品会话和沙箱归属方。Harness 不需要理解宿主平台的沙箱标识、沙箱提供方或持久化模型。 - -模型侧 skill 和工具插件不应自行添加宿主平台特有的请求头。它们调用能力服务;所选提供方负责远程执行和身份传播。这样可以保持模型行为与后端路由之间的职责分离。 - -### 分离异步工作 - -Node ALS 会被 `run()` 内创建的异步资源继承,即使调用方没有等待它们。这对 Agent 所拥有的后台操作很有用,但也可能让无关任务保留陈旧轮次的上下文。 - -身份继承不取代取消归属。在 Agent 边界内启动的工作要么是**前台**的——继承 `{ agent }`,并通过其执行 seam 单独接收显式取消信号;要么是**分离**的——在 `run(undefined, operation)` 下启动,并拥有独立生命周期和显式停止操作。调用方必须让这两个选择保持一致。实现必须记录并测试以下规则: - -- 逻辑上归 Agent 所有的工作是前台工作:可以继承 `{ agent }`,通过现有显式 seam 接收取消,并且必须遵守该 Agent 的 dispose 契约。 -- 与该 Agent 无关的长生命周期部署基础设施、定时器和工作队列是分离工作:必须在 `run(undefined, operation)` 下启动,由自己的归属方停止,绝不因某个轮次结束而被隐式终止。 -- 把数据入队并留待后续处理的代码必须将所需身份序列化到队列项中;不能期待 ALS 跨越队列、进程或 worker 边界。 -- 消费方不能把隐式 Agent 引用视为 Agent 仍然存活的证明。生命周期敏感的操作仍须检查 `agent.status`、显式 signal 或归属服务的契约。 - -`turn` 和 `step` 不进入第一版;如果未来出现真实的横切消费方(追踪、日志)无法使用现有显式字段,可以再将它们作为独立的不可变执行帧扩展引入。完整 `Agent` 是刻意允许的能力例外,因为它就是建立边界的执行主体。每个额外字段都必须是陈旧安全的标签,其陈旧副本最坏只能误标一条追踪记录;其他能力或控制通道需要独立 RFC。第一版不携带 `AbortSignal`;见「考虑过的替代方案」。 - -## 当前 Harness 依据 - -由于这份交接基于本地源码快照编写,目标分支可能已经前进,后续实现会话应在编辑前重新检查这些符号。 - -- `packages/core/agent/src/types.ts`:`Agent` 已经拥有 `session`、`status` 和 `ctx`。其中 `ctx` 的文档将它定义为注册作用域,而不是动态请求上下文。 -- `packages/core/agent/src/index.ts`:Cordis `Context.agent` 作为 Agent 作用域的开发体验关联被安装,在普通上下文上默认返回 `undefined`。不要改变这一语义。 -- `packages/core/agent-loop/src/agent.ts`:`ReactLoopAgent` 已经拥有 inbox、取消逻辑、每步骤 abort、状态和驱动生命周期。不要再创建一套并行的可变运行时状态对象。 -- `packages/core/agent-loop/src/loop.ts`:`runLoop(ctx, agent, handle)` 正好是需要包裹的生命周期边界。它会将 Agent、轮次、步骤和 signal 显式传给更窄的操作。 -- `packages/core/tools/src/index.ts`:`ToolExecutionInput.agent` 是显式字段,并用于选择作用域内的策略和工具解析。增加 ALS 后,它仍然保留在契约中。 -- `packages/core/agent/src/dispatch.ts`:`agentEvents()` 有意把 Agent 主体与其作用域载体融合。隐式上下文不能取代这套正确性机制。 -- `packages/core/README.md` 和现有 core 包:它们表明稳定的 Agent 控制契约位于 `core/`;`agent-execution` 是必载控制基础设施,而不是模型可见的可选上下文增强。 - -本提案扩展而非取代[关于 Agent 注册作用域的既有决策](../../implemented/architecture/2026-07-08-agent-scope-contexts.md)及其[运行时设计](../../implemented/architecture/2026-07-12-agent-scope-runtime-design.md)。 - -## Claude Code 参考实现 - -| Claude Code | Harness 中的对应设计 | -|---|---| -| AppState store | Cordis 部署服务及其拥有的实时状态 | -| QueryEngine | `ReactLoopAgent` 及其 loop 所拥有的运行时状态 | -| ToolUseContext | 能力边界上的显式 Agent、工具和请求参数 | -| AgentContext ALS | 本提案的窄粒度 `AgentExecution` 载体 | -| Transcript | 事件溯源 `Session` 与持久化后端 | - -## 实现交接步骤 - -后续实现会话应按以下顺序开展工作: - -1. 切换到预期目标分支,检查“当前 Harness 依据”中列出文件的当前版本。不要合并或复制编写本交接文档所在分支的修改。 -2. 新增 `packages/core/agent-execution/`,包含包元数据、README、导出类型、Cordis 服务、模块扩展和聚焦测试。 -3. 按照现有包门禁,把该包加入 TypeScript 项目引用、路径候选、运行时闭包或配置以及生成目录。优先使用仓库生成器,不要手工编辑生成文件。同时更新根 `AGENTS.md` 中 repository layout 的 `core/` 行、`packages/core/README.md` 中的包表,以及 `packages/README.md` 中的包组说明。 -4. 让 Agent Loop 声明并消费该服务。在不改变公开 Agent、事件、工具、LLM 或会话签名的前提下,用 `{ agent }` 包裹每个 Agent 驱动的完整 `runLoop` 调用。 -5. 增加集成测试:让同一进程中的两个 Agent 重叠执行,并在至少一次 `await` 后从异步工具执行内部观察到正确的隐式 Agent。 -6. 增加嵌套 Agent 覆盖:证明子 Agent 能看到自己,且子边界结束后父上下文得到恢复。 -7. 增加清除和失败覆盖:边界外返回 `undefined`,`require()` 清晰失败,`run(undefined, ...)` 屏蔽继承的 Agent,抛出异常或 rejected 操作不会污染后续无关工作。 -8. 在集成测试中增加一个能力传输测试替身。保持模型侧 schema 不变,并断言可信会话请求头由内部生成。适配真实生产远程后端属于本 RFC 之外的后续工作。 -9. 运行类型检查、定向测试、文档门禁、生成目录检查,最后运行仓库常规 CI 或 pre-push 门禁。 - -建议的聚焦测试矩阵: - -| 场景 | 必须观察到的结果 | -|---|---| -| 驱动之外 | `current()` 为 `undefined` | -| 一个 Agent 跨越 await | 每个 continuation 都看到完全相同的 Agent | -| 两个并发 Agent | A 永远看不到 B,B 永远看不到 A | -| 嵌套子 Agent | 子 Agent 看到自己;随后恢复父 Agent | -| 子 Agent 创建窗口 | 父工具调用内的创建流程隐式看到父 Agent;`agentCtx.agent` 是子 Agent | -| 直接调用无 Agent 工具 | 显式工具行为仍然有效;隐式身份不存在 | -| 已清除的分离工作 | `run(undefined, ...)` 隐藏继承的 Agent | -| 失败和取消 | throw、rejection 和 abort 后上下文均得到恢复 | -| Agent dispose | 隐式引用不赋予 dispose 后的能力 | -| 服务重载 | Agent 驱动在 ALS disable 前收敛;保留的已 dispose 服务调用抛出文档约定的稳定错误 | -| 能力传输边界 | 会话身份由测试替身传输层写入类型化请求或请求头 | - -## 考虑过的替代方案 - -**让每个函数都传递 Agent。** 对公开边界和承载权限的边界而言,这仍然是正确选择;但如果要求每个私有辅助函数都传递 Agent,就会产生大量样板代码,而隐式执行上下文正适合消除这些代码。本提案在边界处保留显式主体,只在单个可信异步进程内部使用 ALS。 - -**修改 `ctx.agent`,让它返回当前正在执行的 Agent。** 拒绝此方案,因为 `ctx.agent` 已经表示 Agent 作用域 Cordis 上下文的静态关联。让根上下文变成动态语义,会把注册作用域和执行作用域混合起来,在并发时产生意外行为,并破坏已经实现的 Agent 作用域 RFC。 - -**在 ALS 中存储完整的可变运行时对象。** 拒绝此方案,因为 Agent、会话、inbox、取消状态、轮次或步骤状态、工具执行和持久化日志已经有各自的真源。重复保存会产生陈旧快照、写入顺序问题,以及另一套需要清理的生命周期。 - -**在第一版 ALS 帧中携带步骤级 `AbortSignal`。** 本 RFC 拒绝此方案。signal 的生命周期是每步骤,而提议的 ALS 边界是每驱动,因此携带它需要嵌套的步骤和工具边界,还要明确规定分离工作、deadline 归属和恢复语义。现有执行 seam 已经显式传递取消。未来只有在出现具体横切消费方,并通过测试建立这些嵌套生命周期语义后,才可由独立 RFC 重新评估。 - -**使用一个进程级可变 `currentAgent`。** 拒绝此方案,因为并发 Agent 和 subagent 会在 await 边界间相互覆盖。它只有在所有工作严格串行时才正确,而多 Agent 执行明确不保证这一点。 - -**从模型可见的工具参数推导会话。** 拒绝此方案,因为模型可以修改这些参数。沙箱路由和授权需要可信的进程内身份,而不是用户或模型输入。 - -**把宿主平台的沙箱归属标识或提供方数据放入 Harness 上下文。** 拒绝此方案,因为沙箱归属是由 Harness 外部解析的宿主产品状态。Harness 在可信传输边界上传递自己的会话身份即可。 - -## 验收标准 - -- 一个 Node Harness 进程至少能并发执行两个 Agent,异步消费方始终观察到准确的发起 Agent。 -- 在 Agent 驱动执行之外,隐式查询返回 `undefined`,且 `require()` 抛出稳定、可操作的错误。 -- 嵌套 Agent 执行结束后会恢复父上下文。 -- `agent.ctx`、`ctx.agent`、Agent 事件、提示词组装、`ToolExecution.agent`、LLM `sessionId` 和会话持久化保持现有语义。 -- Agent、会话、轮次、步骤、沙箱和授权身份都不能由模型控制。 -- 实现为无关分离任务提供显式 undefined 边界,并通过测试防止上下文泄漏,且不改变现有显式取消契约。 -- 该服务随标准 agent 组合包加载,缺少它时 `dsh-agent-loop` 在加载阶段失败;配置测试锁定这一策略。 -- dispose 或 HMR(热模块替换)会先让所有依赖的 Agent 驱动收敛,再禁用 ALS;已 dispose 服务上的保留调用会抛出文档约定的稳定错误,且已 dispose 的 Cordis 上下文不能继续访问活跃 ALS 状态。 -- 一个能力传输测试替身能证明可信会话 ID 得到传播,同时不新增模型可见的 schema 字段。 -- 包目录、依赖图、API 文档和相关架构文档得到重新生成或更新,仓库文档门禁通过。 - -## 风险 - -- 隐式上下文会从函数签名中隐藏依赖。将它限制在深层横切基础设施,并保留显式公开主体,可以控制这一成本。 -- ALS 对分离 promise 和定时器的继承可能保留语义上陈旧的身份。实现必须提供显式 undefined 边界、文档和回归测试,而不能假设清理会自然发生。 -- ALS 不会跨越 worker thread、子进程、Redis、HTTP 或持久化队列。每个此类边界都必须显式序列化所需身份。 -- 隐式存储有意携带完整的存活 Agent 能力。被捕获的引用可能比 Agent 的发布状态活得更久,因此隐式存在本身绝不授权生命周期敏感工作,消费方仍须遵循 Agent 生命周期和取消契约。 -- 强制加载给每个 agent 组合新增一个核心运行时依赖;本 RFC 接受这一成本,因为可选服务会让隐式身份依赖具体组合。支持范围内的 Node 版本仍存在可测量的传播成本,应另行基准测试。 -- 过早加入轮次、步骤、signal、cwd 或工具细节会扩大继承范围和陈旧状态风险。第一版有意接受只提供 Agent 隐式身份的限制;未来任何额外的能力或控制字段都需要独立 RFC。 diff --git a/examples/coding-agent/tests/code-mode.e2e.ts b/examples/coding-agent/tests/code-mode.e2e.ts index 3f6e109e3f..1994ae9c36 100644 --- a/examples/coding-agent/tests/code-mode.e2e.ts +++ b/examples/coding-agent/tests/code-mode.e2e.ts @@ -9,6 +9,7 @@ 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as ToolBash from '@deepseek-ai/dsh-tool-bash' @@ -44,6 +45,7 @@ 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, { models: ['deepseek-v4-flash'] }) await harness.plugin(LocalBashExecutor, { cwd, timeoutMs: 30_000 }) diff --git a/examples/coding-agent/tests/harness.ts b/examples/coding-agent/tests/harness.ts index dd0bc42a1b..d8790a0443 100644 --- a/examples/coding-agent/tests/harness.ts +++ b/examples/coding-agent/tests/harness.ts @@ -5,6 +5,7 @@ import type { SessionEvent } 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, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as ToolBash from '@deepseek-ai/dsh-tool-bash' @@ -55,6 +56,7 @@ export async function codingHarness(workdir: string, options: CodingHarnessOptio await ctx.plugin(SystemPrompt, { persona: options.persona ?? '' }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] }) await ctx.plugin(LocalBashExecutor, { cwd: workdir, timeoutMs: 30_000 }) diff --git a/examples/cordis-agent/tests/harness.ts b/examples/cordis-agent/tests/harness.ts index 78e5b0bb93..5286957b8a 100644 --- a/examples/cordis-agent/tests/harness.ts +++ b/examples/cordis-agent/tests/harness.ts @@ -4,6 +4,7 @@ import SessionStore 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, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek' import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis' @@ -28,6 +29,7 @@ export async function cordisHarness(): Promise { await ctx.plugin(SystemPrompt, { persona: PERSONA }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] }) await ctx.plugin(ToolCordis) diff --git a/packages/README.md b/packages/README.md index 8f1bf3dde2..55bfa5cc7b 100644 --- a/packages/README.md +++ b/packages/README.md @@ -8,7 +8,7 @@ Packages live at `packages///`; groups are containers, while names r | Group | Role | Release expectation | |---|---|---| -| [`core/`](core/README.md) | Product API spine: session, system-prompt, tools, agent, and the concrete loop | Product — stable surface | +| [`core/`](core/README.md) | Product API spine: session, system-prompt, tools, agent, agent-execution, and the concrete loop | Product — stable surface | | [`llm/`](llm/README.md) | LLM capability family: the abstract service + provider adapters | Product — stable surface | | [`bash/`](bash/README.md) | Bash capability family: the executor seam, a local impl, and the model-facing tool | Product — stable surface | | [`code-runtime/`](code-runtime/README.md) | Code-execution capability family: the abstract runtime seam for model-written programs + a worker-thread backend | Product — stable surface | diff --git a/packages/bash/tool-bash/package.json b/packages/bash/tool-bash/package.json index 035b16aeec..b0a1eaf779 100644 --- a/packages/bash/tool-bash/package.json +++ b/packages/bash/tool-bash/package.json @@ -37,6 +37,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-user-approval": "workspace:^", "@deepseek-ai/dsh-bash": "workspace:^", diff --git a/packages/bash/tool-bash/tests/integration.spec.ts b/packages/bash/tool-bash/tests/integration.spec.ts index 238176b8a6..c07e49c91d 100644 --- a/packages/bash/tool-bash/tests/integration.spec.ts +++ b/packages/bash/tool-bash/tests/integration.spec.ts @@ -6,6 +6,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import TaskService from '@deepseek-ai/dsh-tasks' import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks' @@ -26,6 +27,7 @@ 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: [] }) await ctx.plugin(TaskService) await ctx.plugin(ToolTasks) diff --git a/packages/compact/compact-basic/package.json b/packages/compact/compact-basic/package.json index 32852a060f..55327227c9 100644 --- a/packages/compact/compact-basic/package.json +++ b/packages/compact/compact-basic/package.json @@ -30,6 +30,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-compact": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", diff --git a/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts b/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts index dbf8a3b737..03870c7a6e 100644 --- a/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts +++ b/packages/compact/compact-basic/tests/compact-loop-repro.spec.ts @@ -8,6 +8,7 @@ import { isToolPairingBalanced } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import * as Invariants from '@deepseek-ai/dsh-invariants' import { BasicCompactService } from '@deepseek-ai/dsh-compact-basic' @@ -66,6 +67,7 @@ async function harness(toolSteps: number): Promise<{ ctx: Context; compact: Repr 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 StepwiseToolAdapter(toolSteps)) ctx.tools.register(defineTool({ diff --git a/packages/context/time-context/package.json b/packages/context/time-context/package.json index f319c7a5b1..ee7a60c839 100644 --- a/packages/context/time-context/package.json +++ b/packages/context/time-context/package.json @@ -31,6 +31,7 @@ }, "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/context/time-context/tests/time-context.spec.ts b/packages/context/time-context/tests/time-context.spec.ts index 562b002b68..2b55dd509f 100644 --- a/packages/context/time-context/tests/time-context.spec.ts +++ b/packages/context/time-context/tests/time-context.spec.ts @@ -8,6 +8,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' import type { Agent } from '@deepseek-ai/dsh-agent' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop from '@deepseek-ai/dsh-agent-loop' import * as timeContext from '@deepseek-ai/dsh-time-context' import type { Config } from '@deepseek-ai/dsh-time-context' @@ -94,6 +95,7 @@ async function loopHarness(adapter: ScriptedAdapter, config: Config = {}): 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(timeContext, config) ctx.llm.registerAdapter(['mock'], adapter) diff --git a/packages/cordis/tool-cordis/package.json b/packages/cordis/tool-cordis/package.json index fd9c35e48e..7c5db05b23 100644 --- a/packages/cordis/tool-cordis/package.json +++ b/packages/cordis/tool-cordis/package.json @@ -31,6 +31,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", diff --git a/packages/cordis/tool-cordis/src/api-catalog.ts b/packages/cordis/tool-cordis/src/api-catalog.ts index 51b4a5155f..eb8a25d8b4 100644 --- a/packages/cordis/tool-cordis/src/api-catalog.ts +++ b/packages/cordis/tool-cordis/src/api-catalog.ts @@ -52,6 +52,15 @@ 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 ReactLoopAgent factory and driver service.', @@ -505,6 +514,10 @@ export const TYPE_API: readonly TypeApiEntry[] = [ name: 'Agent', declaration: 'export interface Agent {\n readonly id: AgentId;\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?: SendOptions): 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/cordis/tool-cordis/tests/integration.spec.ts b/packages/cordis/tool-cordis/tests/integration.spec.ts index 94331df7c0..920909c26e 100644 --- a/packages/cordis/tool-cordis/tests/integration.spec.ts +++ b/packages/cordis/tool-cordis/tests/integration.spec.ts @@ -5,6 +5,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import * as ToolCordis from '../src/index.ts' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' @@ -25,6 +26,7 @@ async function harness(adapter: MockAdapter): Promise { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(ToolCordis) ctx.llm.registerAdapter(['mock'], adapter) diff --git a/packages/core/README.md b/packages/core/README.md index 921591d85e..8223b7b0b0 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -9,10 +9,11 @@ The session log, system-prompt assembly, tool registry, agent vocabulary, and co | `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-loop/` | The concrete loop plugin: `ReactLoopAgent` + 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-loop` is the one concrete implementation of the `agent` seam and lives here because it is the harness's default product loop; everything else in `core/` is interface/vocabulary. Plugins depend on the `agent` vocabulary, never on `agent-loop` directly, so the loop stays swappable. +`agent-execution` is mandatory control infrastructure shared by 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; other plugins depend on the `agent` vocabulary and execution service, 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 + invariants + the local [skill family](../skill/README.md) + `tool-bash` + `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 + agent-execution + invariants + the local [skill family](../skill/README.md) + `tool-bash` + `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 new file mode 100644 index 0000000000..a389699863 --- /dev/null +++ b/packages/core/agent-execution/README.md @@ -0,0 +1,23 @@ +# 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 }`. A Session is 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 when the operation does not await them. Agent-owned foreground work may inherit the boundary but keeps using the explicit cancellation and disposal contract of its execution seam. 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 new file mode 100644 index 0000000000..e5ebbbf5f3 --- /dev/null +++ b/packages/core/agent-execution/package.json @@ -0,0 +1,31 @@ +{ + "name": "@deepseek-ai/dsh-agent-execution", + "description": "Agent-scoped asynchronous execution context for the DeepSeek Harness", + "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 new file mode 100644 index 0000000000..3aef4c1856 --- /dev/null +++ b/packages/core/agent-execution/src/index.ts @@ -0,0 +1,139 @@ +/** + * 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 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 (result instanceof Promise) { + 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 new file mode 100644 index 0000000000..ccafb4840a --- /dev/null +++ b/packages/core/agent-execution/src/types.ts @@ -0,0 +1,12 @@ +/** + * Public Agent execution-context types. + * + * @module @deepseek-ai/dsh-agent-execution/types + */ + +import type { Agent } from '@deepseek-ai/dsh-agent' + +/** The exact live 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 new file mode 100644 index 0000000000..ccf17f6722 --- /dev/null +++ b/packages/core/agent-execution/tests/agent-execution.spec.ts @@ -0,0 +1,136 @@ +import { describe, expect, it } from 'vitest' +import { Context } from 'cordis' +import { AgentId, 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' + +function execution(id: string): AgentExecution { + return { agent: { id: AgentId(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') + }) +}) diff --git a/packages/core/agent-execution/tsconfig.json b/packages/core/agent-execution/tsconfig.json new file mode 100644 index 0000000000..a06784e926 --- /dev/null +++ b/packages/core/agent-execution/tsconfig.json @@ -0,0 +1,21 @@ +{ + "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 a07aa5fa70..c961969edc 100644 --- a/packages/core/agent-loop/README.md +++ b/packages/core/agent-loop/README.md @@ -23,7 +23,7 @@ The config-driven `ctx.agentLoop.create()` path keeps its agent owned by the loo ### Injected services -`agents`, `sessions`, `llm`, `tools`, `systemPrompt` — all five interface 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. ### Configuration (schemastery) @@ -48,7 +48,9 @@ Configured agents start automatically. `cwd` applies only to fresh sessions; `re ### Loop lifecycle (`loop.ts`) -The driver owns one agent for its lifetime. It records turn, step, request, stream, and tool boundaries in the session log; live extension events coordinate policy around those durable facts. The [architecture turn flow](../../../docs/architecture.md#turn-flow) and generated [event catalog](../../../docs/cordis-catalog/events.md) are the authoritative sequence and signatures. +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 child boundary; explicit Agent fields remain authoritative at service, worker, process, persistence, and wire boundaries. The [execution-context package](../agent-execution/README.md) owns propagation and detached-work rules. + +The loop records turn, step, request, stream, and tool boundaries in the session log; live extension events coordinate policy around those durable facts. The [architecture turn flow](../../../docs/architecture.md#turn-flow) and generated [event catalog](../../../docs/cordis-catalog/events.md) are the authoritative sequence and signatures. Plugin failure ends the current turn, not the loop. Cancellation clears pending work and aborts the current step without leaking to the next prompt. Terminal continuation stops remain authoritative through turn close and durability flush. diff --git a/packages/core/agent-loop/package.json b/packages/core/agent-loop/package.json index 7e2fb235a2..6a7fe2d937 100644 --- a/packages/core/agent-loop/package.json +++ b/packages/core/agent-loop/package.json @@ -22,6 +22,7 @@ "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", @@ -35,6 +36,7 @@ }, "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 a2288c65b7..7194d6c7e8 100644 --- a/packages/core/agent-loop/src/agent.ts +++ b/packages/core/agent-loop/src/agent.ts @@ -327,7 +327,7 @@ export class ReactLoopAgent implements Agent { [startDriver](): void { if (this._status === 'disposed') return this.driverStarted = true - this.done = runLoop(this.loopCtx, this, { + this.done = this.loopCtx.agentExecution.run({ agent: this }, () => runLoop(this.loopCtx, this, { inbox: this.#inbox, setStatus: (status) => { this.setStatus(status) }, setAbort: controller => void (this.currentAbort = controller), @@ -338,7 +338,7 @@ export class ReactLoopAgent implements Agent { clearCancel: () => { this.cancelRequested = false }, // Pre-step cancellation re-parks without emitting a status transition. settleIdle: () => { this.settleIdleWaiters() }, - }) + })) } /** diff --git a/packages/core/agent-loop/src/index.ts b/packages/core/agent-loop/src/index.ts index 6cd66f622c..51dc0f311f 100644 --- a/packages/core/agent-loop/src/index.ts +++ b/packages/core/agent-loop/src/index.ts @@ -11,6 +11,7 @@ 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 { AgentFactory, AgentHandle, @@ -333,7 +334,7 @@ export interface Config { /** Concrete ReactLoopAgent factory and driver service. */ export class AgentLoop extends Service implements AgentFactory { - static inject = ['agents', 'sessions', 'llm', 'tools', 'systemPrompt'] + static inject = ['agents', 'agentExecution', '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-execution.spec.ts new file mode 100644 index 0000000000..98cebcbc97 --- /dev/null +++ b/packages/core/agent-loop/tests/agent-execution.spec.ts @@ -0,0 +1,374 @@ +import { describe, expect, it } from 'vitest' +import { Context, FiberState, type Fiber } from 'cordis' +import AgentRegistry, { AgentId, 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, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' +import LlmService, { CallId, LlmAdapter } from '@deepseek-ai/dsh-llm' +import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm' +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 { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' + +interface Harness { + ctx: Context + providerFiber: Fiber + loopFiber: Fiber +} + +async function harness(adapter: LlmAdapter): Promise { + 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 providerFiber = await ctx.plugin(AgentExecutionProvider) + const loopFiber = await ctx.plugin(AgentLoop, { agents: [] }) + ctx.llm.registerAdapter(['mock'], adapter) + return { ctx, providerFiber, loopFiber } +} + +function waitForIdle(ctx: Context, agent: ReactLoopAgent | Agent): Promise { + return new Promise((resolve) => { + const dispose = ctx.on('agent/status', (subject, status) => { + if (subject === agent && status === 'idle') { + dispose() + resolve() + } + }) + }) +} + +function send(agent: Agent, text: string): void { + agent.send([{ type: 'text', text }]) +} + +/** Adapter that holds both drivers at the same awaited continuation. */ +class OverlapAdapter extends LlmAdapter { + private readonly bothStarted = Promise.withResolvers() + private starts = 0 + readonly observations: { sessionId: SessionId | undefined; before: Agent; after: Agent }[] = [] + + constructor(private readonly ctx: Context) { + super() + } + + async * stream(options: GenerateOptions): AsyncIterable { + const before = this.ctx.agentExecution.require().agent + 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 + this.observations.push({ sessionId: options.sessionId, before, after }) + yield* textResponse('done') + } +} + +/** Test-only transport that materializes ambient identity at its request boundary. */ +class TestCapabilityTransport { + readonly requests: { path: string; headers: Record }[] = [] + + constructor(private readonly execution: AgentExecutionService) {} + + async request(path: string): Promise> { + await Promise.resolve() + const headers = { + 'X-Harness-Session-Id': this.execution.require().agent.session.id, + } + this.requests.push({ path, headers }) + return headers + } +} + +/** Adapter whose first call waits for cancellation and whose later calls complete. */ +class ReloadAdapter extends LlmAdapter { + readonly firstStarted = Promise.withResolvers() + firstAgentDuringAbort: Agent | undefined + laterAgent: Agent | undefined + calls = 0 + execution: AgentExecutionService | undefined + + async * stream(options: GenerateOptions): AsyncIterable { + const execution = this.execution + if (execution === undefined) throw new Error('execution service missing') + this.calls += 1 + if (this.calls === 1) { + this.firstStarted.resolve(true) + try { + await new Promise((_resolve, reject) => { + const abort = (): void => { reject(new Error('aborted')) } + if (options.signal?.aborted === true) abort() + else options.signal?.addEventListener('abort', abort, { once: true }) + }) + } catch (error: unknown) { + await Promise.resolve() + this.firstAgentDuringAbort = execution.require().agent + throw error + } + return + } + await Promise.resolve() + this.laterAgent = execution.require().agent + yield* textResponse('reloaded') + } +} + +describe('AgentLoop execution context', () => { + it('keeps overlapping driver continuations bound to their exact Agents', async () => { + const ctx = new Context() + const adapter = new OverlapAdapter(ctx) + await ctx.plugin(LlmService) + await ctx.plugin(SessionStore) + 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 a = ctx.agentLoop.create(AgentId('a'), { model: 'mock' }) + const b = ctx.agentLoop.create(AgentId('b'), { model: 'mock' }) + const idleA = waitForIdle(ctx, a) + const idleB = waitForIdle(ctx, b) + send(a, 'a') + send(b, 'b') + await Promise.all([idleA, idleB]) + + expect(adapter.observations).toHaveLength(2) + expect(adapter.observations).toEqual(expect.arrayContaining([ + { sessionId: a.session.id, before: a, after: a }, + { sessionId: b.session.id, before: b, after: b }, + ])) + expect(ctx.agentExecution.current()).toBeUndefined() + await ctx.fiber.dispose() + }) + + it('keeps child setup under the parent boundary, switches for the child driver, then restores the parent', async () => { + const adapter = new MockAdapter([ + toolCallResponse('spawn', 'spawn-child', {}), + toolCallResponse('observe', 'observe-child', {}), + textResponse('child done'), + textResponse('parent done'), + ]) + const { ctx } = await harness(adapter) + let parentDuringSetup: Agent | undefined + let explicitChild: Agent | undefined + let childDuringDriver: Agent | undefined + let parentAfterChild: Agent | undefined + let child: Agent | undefined + + ctx.tools.register(defineTool({ + name: 'spawn-child', + description: 'create one child agent', + parameters: {}, + execute: async (_args, exec) => { + if (exec.agent === undefined) throw new Error('parent agent missing') + const handle = await exec.agent.ctx.agents.create({ + agentId: AgentId('child'), + sessionId: SessionId('child-session'), + agentOptions: { model: 'mock' }, + setup: (agentCtx) => { + parentDuringSetup = ctx.agentExecution.require().agent + explicitChild = agentCtx.agent + agentCtx.tools.register(defineTool({ + name: 'observe-child', + description: 'observe child execution identity', + parameters: {}, + execute: async () => { + await Promise.resolve() + childDuringDriver = ctx.agentExecution.require().agent + return [{ type: 'text', text: 'observed' }] + }, + })) + }, + }) + child = handle.agent + send(handle.agent, 'run child') + await handle.agent.whenIdle() + parentAfterChild = ctx.agentExecution.require().agent + await handle.dispose() + return [{ type: 'text', text: 'child completed' }] + }, + })) + + const parentHandle = await ctx.agents.create({ + agentId: AgentId('parent'), + sessionId: SessionId('parent-session'), + agentOptions: { model: 'mock' }, + }) + const idle = waitForIdle(ctx, parentHandle.agent) + send(parentHandle.agent, 'spawn') + await idle + + expect(parentDuringSetup).toBe(parentHandle.agent) + expect(explicitChild).toBe(child) + expect(childDuringDriver).toBe(child) + expect(parentAfterChild).toBe(parentHandle.agent) + expect(ctx.agentExecution.current()).toBeUndefined() + await parentHandle.dispose() + await ctx.fiber.dispose() + }) + + it('keeps agentless direct tools ambient-free and builds trusted transport headers internally', async () => { + const adapter = new MockAdapter([ + toolCallResponse('capability', 'capability-request', { path: '/v1/capability' }), + textResponse('done'), + ]) + const { ctx } = await harness(adapter) + const transport = new TestCapabilityTransport(ctx.agentExecution) + let directAmbient: Agent | undefined + let captured: Agent | undefined + + ctx.tools.register(defineTool({ + name: 'agentless-probe', + description: 'observe an agentless call', + parameters: {}, + execute: async () => { + await Promise.resolve() + directAmbient = ctx.agentExecution.current()?.agent + return [{ type: 'text', text: 'ok' }] + }, + })) + ctx.tools.register(defineTool({ + name: 'capability-request', + description: 'call the test capability transport', + parameters: { path: { type: 'string' } }, + execute: async (args) => { + captured = ctx.agentExecution.require().agent + const path = (args as { path: string }).path + const headers = await transport.request(path) + return [{ type: 'text', text: JSON.stringify(headers) }] + }, + })) + + const direct = await ctx.tools.execute({ + callId: CallId('direct'), + name: 'agentless-probe', + arguments: {}, + }) + expect(direct.isError).toBe(false) + expect(directAmbient).toBeUndefined() + + const handle = await ctx.agents.create({ + agentId: AgentId('transport'), + sessionId: SessionId('transport-session'), + agentOptions: { model: 'mock' }, + }) + const idle = waitForIdle(ctx, handle.agent) + send(handle.agent, 'call transport') + await idle + + expect(transport.requests).toEqual([{ + path: '/v1/capability', + headers: { 'X-Harness-Session-Id': 'transport-session' }, + }]) + const schema = adapter.requests[0]?.tools?.find(tool => tool.name === 'capability-request') + expect(JSON.stringify(schema?.parameters)).not.toMatch(/session|harness/i) + const call = handle.agent.session.events.find(event => event.type === 'tool/call') + expect(call?.type === 'tool/call' ? call.data.arguments : undefined) + .toBe(JSON.stringify({ path: '/v1/capability' })) + expect(captured).toBe(handle.agent) + + await handle.dispose() + expect(captured?.status).toBe('disposed') + expect(ctx.agentExecution.current()).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() + 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 oldHandle = await ctx.agents.create({ + agentId: AgentId('before-restart'), + sessionId: SessionId('before-restart-session'), + agentOptions: { model: 'mock' }, + }) + const oldAgent = oldHandle.agent + send(oldAgent, 'block') + await adapter.firstStarted.promise + + await providerFiber.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 + + const newHandle = await ctx.agents.create({ + agentId: AgentId('after-restart'), + sessionId: SessionId('after-restart-session'), + agentOptions: { model: 'mock' }, + }) + const newAgent = newHandle.agent + const idle = waitForIdle(ctx, newAgent) + send(newAgent, 'continue') + await idle + expect(adapter.laterAgent?.id).toBe(newAgent.id) + expect(adapter.laterAgent?.session).toBe(newAgent.session) + await ctx.fiber.dispose() + }) + + it('keeps ALS readable while root disposal drains sibling AgentLoop fibers', async () => { + const ctx = new Context() + const adapter = new ReloadAdapter() + await ctx.plugin(LlmService) + await ctx.plugin(SessionStore) + 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 handle = await ctx.agents.create({ + agentId: AgentId('root-dispose'), + sessionId: SessionId('root-dispose-session'), + agentOptions: { model: 'mock' }, + }) + const agent = handle.agent + send(agent, 'block') + await adapter.firstStarted.promise + + await ctx.fiber.dispose() + 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') + }) +}) diff --git a/packages/core/agent-loop/tests/agent.spec.ts b/packages/core/agent-loop/tests/agent.spec.ts index 581b6fa207..bcc31a6172 100644 --- a/packages/core/agent-loop/tests/agent.spec.ts +++ b/packages/core/agent-loop/tests/agent.spec.ts @@ -6,6 +6,7 @@ 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, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { bindReactLoopAgentContext, prepareReactLoopAgent } from '../src/agent.ts' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -17,6 +18,7 @@ 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 @@ -51,6 +53,7 @@ function send(agent: ReactLoopAgent, text: string) { describe('ReactLoopAgent', () => { 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(ctx, AgentId('first-driver'), { model: 'mock' }, session) @@ -252,6 +255,7 @@ describe('ReactLoopAgent', () => { it('disposer is idempotent (double-stop)', async () => { // The internal start seam exposes one idle driver's disposer for repeated invocation. const ctx = new Context() + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(SessionStore) const session = ctx.sessions.create(SessionId('test')) const prepared = prepareReactLoopAgent(ctx, AgentId('bare'), { model: 'mock' }, session) @@ -361,6 +365,7 @@ describe('ReactLoopAgent', () => { // Queue the internal waiter while running, then dispose the bare driver. Its disposed branch // must chain the loop's `done` promise rather than resolve before exit. 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 cad830e827..7568ba0765 100644 --- a/packages/core/agent-loop/tests/cancel.spec.ts +++ b/packages/core/agent-loop/tests/cancel.spec.ts @@ -14,6 +14,7 @@ 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -24,6 +25,7 @@ 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 @@ -194,6 +196,7 @@ 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) @@ -319,6 +322,7 @@ 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 8a5d9ce264..182a76a7e9 100644 --- a/packages/core/agent-loop/tests/config-session-id.spec.ts +++ b/packages/core/agent-loop/tests/config-session-id.spec.ts @@ -9,6 +9,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -31,6 +32,7 @@ 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: AgentId('main'), model: 'mock', resumeSessionId: SessionId('deferred') }], }) @@ -53,6 +55,7 @@ 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: AgentId('cfg'), model: 'mock' }] }) await ctx1.plugin(SessionPersistenceJsonl, { root }) ctx1.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg')])) @@ -70,6 +73,7 @@ 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: AgentId('cfg'), model: 'mock' }] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('cfg2')])) @@ -93,6 +97,7 @@ 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')])) @@ -109,6 +114,7 @@ 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: AgentId('main'), model: 'mock', resumeSessionId: SessionId('sticky-1') }] }) await ctx2.plugin(SessionPersistenceJsonl, { root }) ctx2.llm.registerAdapter(['mock'], new MockAdapter([textResponse('second')])) @@ -137,6 +143,7 @@ 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: AgentId('main'), 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 ee50261e9d..70528a7b0b 100644 --- a/packages/core/agent-loop/tests/contract-regressions.spec.ts +++ b/packages/core/agent-loop/tests/contract-regressions.spec.ts @@ -5,6 +5,7 @@ import SessionStore, { Session, SessionEvent, SessionId, TurnEndReason } from '@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId, type ContinuationDecision } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { prepareReactLoopAgent } from '../src/agent.ts' import * as Invariants from '@deepseek-ai/dsh-invariants' @@ -19,6 +20,7 @@ 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 @@ -524,6 +526,7 @@ 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) @@ -664,6 +667,7 @@ 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) @@ -1114,6 +1118,7 @@ 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) @@ -1165,6 +1170,7 @@ 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) @@ -1220,6 +1226,7 @@ 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) @@ -1271,6 +1278,7 @@ 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) @@ -1320,6 +1328,7 @@ 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 5deee8e159..8c0884d0fa 100644 --- a/packages/core/agent-loop/tests/coverage-edges.spec.ts +++ b/packages/core/agent-loop/tests/coverage-edges.spec.ts @@ -6,6 +6,7 @@ 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -16,6 +17,7 @@ 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 4b37210993..0c8f52c6c4 100644 --- a/packages/core/agent-loop/tests/interception.spec.ts +++ b/packages/core/agent-loop/tests/interception.spec.ts @@ -10,6 +10,7 @@ import AgentRegistry, { type PromptDecision, type SessionStartSource, } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -29,6 +30,7 @@ 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 fb686928b1..79f9755220 100644 --- a/packages/core/agent-loop/tests/loop.spec.ts +++ b/packages/core/agent-loop/tests/loop.spec.ts @@ -5,6 +5,7 @@ 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, maxTokensResponse, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -15,6 +16,7 @@ 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 @@ -924,6 +926,7 @@ 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: AgentId('config-agent'), model: 'mock' }], }) @@ -947,6 +950,7 @@ 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: AgentId('config-agent'), 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 dbc43ad985..d100000d14 100644 --- a/packages/core/agent-loop/tests/properties.spec.ts +++ b/packages/core/agent-loop/tests/properties.spec.ts @@ -13,6 +13,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import fc from 'fast-check' @@ -36,6 +37,7 @@ 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 9b8513d16f..3b454236f1 100644 --- a/packages/core/agent-loop/tests/request-cache.e2e.ts +++ b/packages/core/agent-loop/tests/request-cache.e2e.ts @@ -5,6 +5,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId, 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' @@ -42,6 +43,7 @@ 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, { models: ['deepseek-v4-flash'] }) 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 c4fe471bd0..58c1017e81 100644 --- a/packages/core/agent-loop/tests/request-reconstruction.spec.ts +++ b/packages/core/agent-loop/tests/request-reconstruction.spec.ts @@ -14,6 +14,7 @@ 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -24,6 +25,7 @@ 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 3747824231..4b90453909 100644 --- a/packages/core/agent-loop/tests/resume.spec.ts +++ b/packages/core/agent-loop/tests/resume.spec.ts @@ -10,6 +10,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -29,6 +30,7 @@ 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) @@ -138,6 +140,7 @@ 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) @@ -166,6 +169,7 @@ 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) @@ -388,6 +392,7 @@ 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')])) @@ -449,6 +454,7 @@ 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) @@ -502,6 +508,7 @@ 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) @@ -531,6 +538,7 @@ 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) @@ -561,6 +569,7 @@ 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({ agentId: AgentId('m'), 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 40272ac42b..53e91defff 100644 --- a/packages/core/agent-loop/tests/scope-lifecycle.spec.ts +++ b/packages/core/agent-loop/tests/scope-lifecycle.spec.ts @@ -7,6 +7,7 @@ import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId, agentEvents, assembleContextFor } from '@deepseek-ai/dsh-agent' 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, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import type { ContentBlock } from '@deepseek-ai/dsh-llm' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -18,6 +19,7 @@ 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-order.spec.ts b/packages/core/agent-loop/tests/tool-order.spec.ts index 13e731b7b8..813f7892d9 100644 --- a/packages/core/agent-loop/tests/tool-order.spec.ts +++ b/packages/core/agent-loop/tests/tool-order.spec.ts @@ -14,6 +14,7 @@ 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { MockAdapter, textResponse } from './mock-adapter.ts' @@ -24,6 +25,7 @@ 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 c275fee88c..944c6fd265 100644 --- a/packages/core/agent-loop/tests/turn-stop.spec.ts +++ b/packages/core/agent-loop/tests/turn-stop.spec.ts @@ -5,6 +5,7 @@ import SessionStore, { type TurnEndReason } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry, { defineTool } from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId, type ContinuationStop } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import * as Invariants from '@deepseek-ai/dsh-invariants' import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts' @@ -17,6 +18,7 @@ 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 5d7cf98bb7..1e17efa41f 100644 --- a/packages/core/agent-loop/tsconfig.json +++ b/packages/core/agent-loop/tsconfig.json @@ -35,6 +35,9 @@ { "path": "../../core/agent" }, + { + "path": "../../core/agent-execution" + }, { "path": "../../core/scope" } diff --git a/packages/core/agent/tests/gen-cordis-catalog.spec.ts b/packages/core/agent/tests/gen-cordis-catalog.spec.ts index 856ef899f7..53847b0e97 100644 --- a/packages/core/agent/tests/gen-cordis-catalog.spec.ts +++ b/packages/core/agent/tests/gen-cordis-catalog.spec.ts @@ -160,6 +160,26 @@ export class FixService { 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).toEqual(['run(id: string): string']) + }) + it('hard-errors on a public method with no JSDoc at all', () => { expect(() => collectServices(makeService( '/** Fixture service. */\nexport class FixService {\n run(id: string): string { return id }\n}', diff --git a/packages/examples/README.md b/packages/examples/README.md index 5ee0206d0f..71867e0da6 100644 --- a/packages/examples/README.md +++ b/packages/examples/README.md @@ -4,7 +4,7 @@ Pre-composed plugin bundles a thin leaf `cordis.yml` loads instead of assembling | Package | npm name | Role | |---|---|---| -| `agent-spine-demo/` | `@deepseek-ai/dsh-agent-spine-demo` | The executor-less/UI-less agent spine as one bundle plugin (`timer` + `llm` + sessions + system-prompt + tools + skills + agents + invariants + `tool-bash` + `tool-skill` + `agent-loop`) | +| `agent-spine-demo/` | `@deepseek-ai/dsh-agent-spine-demo` | The executor-less/UI-less agent spine as one bundle plugin (`timer` + `llm` + sessions + system-prompt + tools + skills + agents + agent-execution + invariants + `tool-bash` + `tool-skill` + `agent-loop`) | | `stdio-demo/` | `@deepseek-ai/dsh-stdio-demo` | Terminal stdio chat app: the spine + console logger + readline UI + a pre-created `main` agent, with a boot `bin` | | `acp-demo/` | `@deepseek-ai/dsh-acp-demo` | ACP server app: the spine + JSONL persistence + the [`acp`](../ui/acp/README.md) bridge (no stdout logger), with a boot `bin` | | `jsonrpc-demo/` | `@deepseek-ai/dsh-jsonrpc-demo` | Bin-only runtime that boots an external `cordis.yml` for the stdio JSON-RPC SDK client | diff --git a/packages/examples/agent-spine-demo/README.md b/packages/examples/agent-spine-demo/README.md index eb56f9515c..442b8e1366 100644 --- a/packages/examples/agent-spine-demo/README.md +++ b/packages/examples/agent-spine-demo/README.md @@ -17,6 +17,7 @@ Read this package for the whole plugin tree and its composition order. @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-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 d328043507..84e4259741 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 + tasks + invariants + tool-bash + 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 + agent-execution + tasks + invariants + tool-bash + tool-skill + tool-tasks + agent-loop)", "version": "0.0.1", "private": true, "type": "module", @@ -24,6 +24,7 @@ "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-llm": "^0.0.1", @@ -41,6 +42,7 @@ "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-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", diff --git a/packages/examples/agent-spine-demo/src/index.ts b/packages/examples/agent-spine-demo/src/index.ts index b3d6ca22bc..439897b347 100644 --- a/packages/examples/agent-spine-demo/src/index.ts +++ b/packages/examples/agent-spine-demo/src/index.ts @@ -18,6 +18,7 @@ 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' @@ -116,6 +117,7 @@ export function apply(ctx: Context, config: Config): void { ctx.plugin(SkillService, config.skills?.registry ?? {}) ctx.plugin(SkillLocal, config.skills?.local ?? {}) ctx.plugin(AgentRegistry) + ctx.plugin(AgentExecutionProvider) ctx.plugin(TaskService) ctx.plugin(invariants) ctx.plugin(toolBash, config.toolBash ?? {}) diff --git a/packages/examples/agent-spine-demo/tsconfig.json b/packages/examples/agent-spine-demo/tsconfig.json index faea4b949f..09cad875f1 100644 --- a/packages/examples/agent-spine-demo/tsconfig.json +++ b/packages/examples/agent-spine-demo/tsconfig.json @@ -41,6 +41,9 @@ { "path": "../../core/agent" }, + { + "path": "../../core/agent-execution" + }, { "path": "../../core/agent-loop" }, diff --git a/packages/fs/tool-fs/package.json b/packages/fs/tool-fs/package.json index c21c806e98..98439e8163 100644 --- a/packages/fs/tool-fs/package.json +++ b/packages/fs/tool-fs/package.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-fs-policy": "workspace:^", "@deepseek-ai/dsh-fs": "workspace:^", diff --git a/packages/fs/tool-fs/tests/harness.ts b/packages/fs/tool-fs/tests/harness.ts index 0487962922..4dc4bb8437 100644 --- a/packages/fs/tool-fs/tests/harness.ts +++ b/packages/fs/tool-fs/tests/harness.ts @@ -4,6 +4,7 @@ import SessionStore 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 LocalFileSystem from '@deepseek-ai/dsh-fs-local' import * as FsPolicy from '@deepseek-ai/dsh-fs-policy' @@ -22,6 +23,7 @@ export async function fsHarness(fsCwd: string, persona = ''): Promise { await ctx.plugin(SystemPrompt, { persona }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] }) await ctx.plugin(LocalFileSystem, { cwd: fsCwd }) diff --git a/packages/guard/repeat-tool-guard/package.json b/packages/guard/repeat-tool-guard/package.json index 0cc99b6976..8df2eac25f 100644 --- a/packages/guard/repeat-tool-guard/package.json +++ b/packages/guard/repeat-tool-guard/package.json @@ -31,6 +31,7 @@ }, "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/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts b/packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts index 565f1076b5..355c5ed2a8 100644 --- a/packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts +++ b/packages/guard/repeat-tool-guard/tests/repeat-tool-guard.spec.ts @@ -5,6 +5,7 @@ import SessionStore, { 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import * as RepeatToolGuard from '@deepseek-ai/dsh-repeat-tool-guard' import type { Config } from '@deepseek-ai/dsh-repeat-tool-guard' @@ -26,6 +27,7 @@ async function harness(config: Config = {}): Promise { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(RepeatToolGuard, config) ctx.tools.register(defineTool({ name: 'probe', description: 'p', parameters: {}, async execute() { return [{ type: 'text', text: 'ok' }] } })) @@ -368,6 +370,7 @@ describe('config validation fails loud', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) return ctx } diff --git a/packages/hooks/hooks-claude/package.json b/packages/hooks/hooks-claude/package.json index 21f08965d8..03858f1aaf 100644 --- a/packages/hooks/hooks-claude/package.json +++ b/packages/hooks/hooks-claude/package.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", diff --git a/packages/hooks/hooks-claude/tests/bridge.spec.ts b/packages/hooks/hooks-claude/tests/bridge.spec.ts index 506d613c73..c298a8d3f3 100644 --- a/packages/hooks/hooks-claude/tests/bridge.spec.ts +++ b/packages/hooks/hooks-claude/tests/bridge.spec.ts @@ -9,6 +9,7 @@ import SessionStore, { 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as HooksClaude from '@deepseek-ai/dsh-hooks-claude' @@ -47,6 +48,7 @@ async function harnessWithFiber(configDir: string, adapter: MockAdapter): Promis await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) const hooks = await ctx.plugin(HooksClaude, { configPath: join(configDir, 'hooks.json') }) @@ -342,6 +344,7 @@ describe('hooks-claude bridge — load resilience', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) await ctx.plugin(HooksClaude, { configPath: '/nonexistent/hooks.json' }) @@ -364,6 +367,7 @@ describe('hooks-claude bridge — load resilience', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) const fiber = await ctx.plugin(HooksClaude, { configPath: join(dir, 'hooks.json') }) diff --git a/packages/hooks/hooks-claude/tests/coverage.spec.ts b/packages/hooks/hooks-claude/tests/coverage.spec.ts index f376708688..db61f4846f 100644 --- a/packages/hooks/hooks-claude/tests/coverage.spec.ts +++ b/packages/hooks/hooks-claude/tests/coverage.spec.ts @@ -8,6 +8,7 @@ import SessionStore, { 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as HooksClaude from '@deepseek-ai/dsh-hooks-claude' @@ -35,6 +36,7 @@ async function harness(configPath: string, adapter: MockAdapter, opts: HarnessOp await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) await ctx.plugin(HooksClaude, { configPath, ...opts }) @@ -334,6 +336,7 @@ describe('hooks-claude coverage — schema-bypass apply + unspawnable hook', () await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) // Direct apply with only configPath — bypasses schemastery's defaults, so @@ -599,6 +602,7 @@ describe('hooks-claude coverage — hook runs in the session cwd, not the server await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) // Executor default cwd = serverDir (deliberately NOT the session cwd). await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000, cwd: serverDir }) @@ -632,6 +636,7 @@ describe('hooks-claude coverage — hook runs in the session cwd, not the server await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) // Executor default cwd = serverDir (deliberately NOT the child session cwd). await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000, cwd: serverDir }) diff --git a/packages/hooks/hooks-codex/package.json b/packages/hooks/hooks-codex/package.json index fe667b0302..1928b763ea 100644 --- a/packages/hooks/hooks-codex/package.json +++ b/packages/hooks/hooks-codex/package.json @@ -34,6 +34,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", diff --git a/packages/hooks/hooks-codex/tests/bridge.spec.ts b/packages/hooks/hooks-codex/tests/bridge.spec.ts index d4a5797b96..fcf0617bff 100644 --- a/packages/hooks/hooks-codex/tests/bridge.spec.ts +++ b/packages/hooks/hooks-codex/tests/bridge.spec.ts @@ -9,6 +9,7 @@ import SessionStore, { 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as HooksCodex from '@deepseek-ai/dsh-hooks-codex' @@ -45,6 +46,7 @@ async function harness(dir: string, adapter: MockAdapter): Promise { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) await ctx.plugin(HooksCodex, { configPath: join(dir, 'hooks.json'), model: 'test-model' }) @@ -146,6 +148,7 @@ describe('hooks-codex bridge', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) const fiber = await ctx.plugin(HooksCodex, { configPath: join(dir, 'hooks.json'), model: 'm' }) @@ -172,6 +175,7 @@ describe('hooks-codex bridge', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) const fiber = await ctx.plugin(HooksCodex, { configPath: join(dir, 'hooks.json'), model: 'm' }) diff --git a/packages/hooks/hooks-codex/tests/coverage.spec.ts b/packages/hooks/hooks-codex/tests/coverage.spec.ts index c287d86b23..1dec4a4886 100644 --- a/packages/hooks/hooks-codex/tests/coverage.spec.ts +++ b/packages/hooks/hooks-codex/tests/coverage.spec.ts @@ -8,6 +8,7 @@ import SessionStore, { 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, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { type ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as HooksCodex from '@deepseek-ai/dsh-hooks-codex' @@ -26,6 +27,7 @@ function hooks(d: string, h: unknown): string { async function harness(configPath: string, adapter: MockAdapter, opts: { stderrSummaryMaxChars?: number } = {}): Promise { const ctx = new Context() await ctx.plugin(LlmService); await ctx.plugin(SessionStore); await ctx.plugin(SystemPrompt) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(ToolRegistry); await ctx.plugin(AgentRegistry); await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) await ctx.plugin(HooksCodex, { configPath, model: 'm', ...opts }) @@ -239,6 +241,7 @@ describe('hooks-codex coverage — decision mapping paths', () => { const adapter = new MockAdapter([textResponse('ok')]) const ctx = new Context() await ctx.plugin(LlmService); await ctx.plugin(SessionStore); await ctx.plugin(SystemPrompt) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(ToolRegistry); await ctx.plugin(AgentRegistry); await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000 }) ctx.logger.warn = warn as never @@ -544,6 +547,7 @@ describe('hooks-codex coverage — decision mapping paths', () => { const adapter = new MockAdapter([toolCallResponse('c1', 'Bash', { command: 'x' }), textResponse('done')]) const ctx = new Context() await ctx.plugin(LlmService); await ctx.plugin(SessionStore); await ctx.plugin(SystemPrompt) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(ToolRegistry); await ctx.plugin(AgentRegistry); await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LocalBashExecutor, { timeoutMs: 10_000, cwd: serverDir }) await ctx.plugin(HooksCodex, { configPath: join(serverDir, 'hooks.json'), model: 'm' }) diff --git a/packages/sdk/helper/src/features/builtin/spine.ts b/packages/sdk/helper/src/features/builtin/spine.ts index caf066865e..d2fb877ec4 100644 --- a/packages/sdk/helper/src/features/builtin/spine.ts +++ b/packages/sdk/helper/src/features/builtin/spine.ts @@ -36,6 +36,10 @@ 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/subagent/subagent-fork/package.json b/packages/subagent/subagent-fork/package.json index 8794884518..3cfecd435c 100644 --- a/packages/subagent/subagent-fork/package.json +++ b/packages/subagent/subagent-fork/package.json @@ -33,6 +33,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", diff --git a/packages/subagent/subagent-fork/tests/multi-subagent.spec.ts b/packages/subagent/subagent-fork/tests/multi-subagent.spec.ts index 8060a77fd4..b4066ff932 100644 --- a/packages/subagent/subagent-fork/tests/multi-subagent.spec.ts +++ b/packages/subagent/subagent-fork/tests/multi-subagent.spec.ts @@ -5,6 +5,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } 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 SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent' @@ -32,6 +33,7 @@ async function setup(script: Script) { await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(Invariants) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) await ctx.plugin(Spawn, { providerName: 'spawn' }) diff --git a/packages/subagent/subagent-fork/tests/subagent-fork.spec.ts b/packages/subagent/subagent-fork/tests/subagent-fork.spec.ts index b5e129e54f..7871f5d13d 100644 --- a/packages/subagent/subagent-fork/tests/subagent-fork.spec.ts +++ b/packages/subagent/subagent-fork/tests/subagent-fork.spec.ts @@ -6,6 +6,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } 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 SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent' @@ -39,6 +40,7 @@ async function setup(script: Script) { await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(Invariants) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) await ctx.plugin(fork, { providerName: 'fork' }) diff --git a/packages/subagent/subagent-inprocess/package.json b/packages/subagent/subagent-inprocess/package.json index aa80dcac3e..6676a3992c 100644 --- a/packages/subagent/subagent-inprocess/package.json +++ b/packages/subagent/subagent-inprocess/package.json @@ -32,6 +32,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", diff --git a/packages/subagent/subagent-inprocess/tests/structured.spec.ts b/packages/subagent/subagent-inprocess/tests/structured.spec.ts index a2c55995c2..e8d19fe4c5 100644 --- a/packages/subagent/subagent-inprocess/tests/structured.spec.ts +++ b/packages/subagent/subagent-inprocess/tests/structured.spec.ts @@ -6,6 +6,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' import type { ContinuationDecision } 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 SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent' @@ -56,6 +57,7 @@ async function setup(script: Script, options: SetupOptions = {}) { } await ctx.plugin(AgentRegistry) await ctx.plugin(Invariants) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) const disposeProvider = ctx.subagents.registerProvider({ diff --git a/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts b/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts index 0005246523..cecad34bfc 100644 --- a/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts +++ b/packages/subagent/subagent-inprocess/tests/subagent-inprocess.spec.ts @@ -5,6 +5,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId, 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 Invariants from '@deepseek-ai/dsh-invariants' import SubagentService from '@deepseek-ai/dsh-subagent' @@ -21,6 +22,7 @@ async function setup(script: Script) { await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(Invariants) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) ctx.llm.registerAdapter(['mock'], new MockAdapter(script)) diff --git a/packages/subagent/subagent-spawn/package.json b/packages/subagent/subagent-spawn/package.json index f2500a4a56..c4629e72c2 100644 --- a/packages/subagent/subagent-spawn/package.json +++ b/packages/subagent/subagent-spawn/package.json @@ -31,6 +31,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", diff --git a/packages/subagent/subagent-spawn/tests/harness.ts b/packages/subagent/subagent-spawn/tests/harness.ts index e4ee2e6ab7..4d47900663 100644 --- a/packages/subagent/subagent-spawn/tests/harness.ts +++ b/packages/subagent/subagent-spawn/tests/harness.ts @@ -4,6 +4,7 @@ import SessionStore 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 { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' import * as ToolBash from '@deepseek-ai/dsh-tool-bash' @@ -30,6 +31,7 @@ export async function spawnHarness(workdir: string): Promise { await ctx.plugin(SystemPrompt, { persona: 'You are a coding agent. Report only when the requested work is done.' }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] }) await ctx.plugin(LocalBashExecutor, { cwd: workdir, timeoutMs: 30_000 }) diff --git a/packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts b/packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts index 43ec8ee1ac..df93357dbc 100644 --- a/packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts +++ b/packages/subagent/subagent-spawn/tests/subagent-spawn.spec.ts @@ -7,6 +7,7 @@ import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' import { SessionId } from '@deepseek-ai/dsh-session' +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 SubagentService, { type SubagentStartRequest } from '@deepseek-ai/dsh-subagent' @@ -32,6 +33,7 @@ async function setup(script: Script) { await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(Invariants) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) await ctx.plugin(spawn, { providerName: 'spawn' }) @@ -309,6 +311,7 @@ describe('dsh-subagent-spawn', () => { await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(Invariants) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) const fiber = await ctx.plugin(spawn, { providerName: 'spawn' }) @@ -341,6 +344,7 @@ describe('dsh-subagent-spawn', () => { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) const fiber = await ctx.plugin(spawn, { providerName: 'spawn' }) diff --git a/packages/todo/tool-todo/package.json b/packages/todo/tool-todo/package.json index 9ff69d7c76..dd1fbdbcc6 100644 --- a/packages/todo/tool-todo/package.json +++ b/packages/todo/tool-todo/package.json @@ -29,6 +29,7 @@ }, "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/todo/tool-todo/tests/integration.spec.ts b/packages/todo/tool-todo/tests/integration.spec.ts index 739367a699..52ebc57683 100644 --- a/packages/todo/tool-todo/tests/integration.spec.ts +++ b/packages/todo/tool-todo/tests/integration.spec.ts @@ -6,6 +6,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } from '@deepseek-ai/dsh-agent' +import AgentExecutionProvider from '@deepseek-ai/dsh-agent-execution' import AgentLoop, { ReactLoopAgent } from '@deepseek-ai/dsh-agent-loop' import * as ToolTodo from '@deepseek-ai/dsh-tool-todo' import { MockAdapter, textResponse, toolCallResponse } from '../../../core/agent-loop/tests/mock-adapter.ts' @@ -23,6 +24,7 @@ async function harness(adapter: MockAdapter): Promise { await ctx.plugin(SystemPrompt) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(ToolTodo) ctx.llm.registerAdapter(['mock'], adapter) diff --git a/packages/ui/acp/package.json b/packages/ui/acp/package.json index 71efc51a07..173c68a66c 100644 --- a/packages/ui/acp/package.json +++ b/packages/ui/acp/package.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", diff --git a/packages/ui/acp/tests/harness.ts b/packages/ui/acp/tests/harness.ts index 440120943f..be019b1e92 100644 --- a/packages/ui/acp/tests/harness.ts +++ b/packages/ui/acp/tests/harness.ts @@ -11,6 +11,7 @@ import SessionStore 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 SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl' import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local' @@ -193,6 +194,7 @@ export async function makeBridgeHarness(options: { await ctx.plugin(SystemPrompt, { persona: options.persona ?? '' }) await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SessionPersistenceJsonl, { root: options.storageDir }) await ctx.plugin(UserInteractionService) diff --git a/packages/workflow/workflow-workerthread/package.json b/packages/workflow/workflow-workerthread/package.json index 485e252c97..4c07c8f118 100644 --- a/packages/workflow/workflow-workerthread/package.json +++ b/packages/workflow/workflow-workerthread/package.json @@ -41,6 +41,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-agent": "workspace:^", + "@deepseek-ai/dsh-agent-execution": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", diff --git a/packages/workflow/workflow-workerthread/tests/integration.spec.ts b/packages/workflow/workflow-workerthread/tests/integration.spec.ts index 0e1727f877..5897342535 100644 --- a/packages/workflow/workflow-workerthread/tests/integration.spec.ts +++ b/packages/workflow/workflow-workerthread/tests/integration.spec.ts @@ -5,6 +5,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } 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 SubagentService from '@deepseek-ai/dsh-subagent' @@ -32,6 +33,7 @@ async function setup(script: Script) { await ctx.plugin(ToolRegistry) await ctx.plugin(AgentRegistry) await ctx.plugin(Invariants) + await ctx.plugin(AgentExecutionProvider) await ctx.plugin(AgentLoop, { agents: [] }) await ctx.plugin(SubagentService) await ctx.plugin(spawn, { providerName: 'spawn' }) diff --git a/packages/workflow/workflow-workerthread/tests/workflow-workerthread.e2e.ts b/packages/workflow/workflow-workerthread/tests/workflow-workerthread.e2e.ts index 1d6f61432a..f502798194 100644 --- a/packages/workflow/workflow-workerthread/tests/workflow-workerthread.e2e.ts +++ b/packages/workflow/workflow-workerthread/tests/workflow-workerthread.e2e.ts @@ -5,6 +5,7 @@ import SessionStore from '@deepseek-ai/dsh-session' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRegistry from '@deepseek-ai/dsh-tools' import AgentRegistry, { AgentId } 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' @@ -34,6 +35,7 @@ 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, { models: ['deepseek-v4-flash'] }) await built.plugin(SubagentService) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b1a3a78030..37b9a70edc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ 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 @@ -225,6 +228,9 @@ 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 @@ -259,6 +265,9 @@ 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 @@ -293,6 +302,9 @@ 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 @@ -336,6 +348,15 @@ 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: @@ -345,6 +366,9 @@ 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 @@ -492,6 +516,9 @@ 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 @@ -645,6 +672,9 @@ 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 @@ -685,6 +715,9 @@ 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 @@ -725,6 +758,9 @@ 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 @@ -765,6 +801,9 @@ 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 @@ -819,7 +858,7 @@ importers: dependencies: '@earendil-works/pi-ai': specifier: ^0.79.1 - version: 0.79.3(ws@8.21.0)(zod@4.4.3) + version: 0.79.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3) schemastery: specifier: ^3.18.0 version: 3.18.0 @@ -1149,6 +1188,9 @@ 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 @@ -1185,6 +1227,9 @@ 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 @@ -1222,6 +1267,9 @@ 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 @@ -1461,6 +1509,9 @@ 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 @@ -1495,6 +1546,9 @@ 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 @@ -1891,6 +1945,9 @@ 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 @@ -1945,6 +2002,9 @@ 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 @@ -3059,6 +3119,10 @@ packages: cpu: [x64] os: [win32] + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -6137,11 +6201,11 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@earendil-works/pi-ai@0.79.3(ws@8.21.0)(zod@4.4.3)': + '@earendil-works/pi-ai@0.79.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.21.0)(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) '@aws-sdk/client-bedrock-runtime': 3.1048.0 - '@google/genai': 1.52.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) '@mistralai/mistralai': 2.2.1 '@smithy/node-http-handler': 4.7.3 http-proxy-agent: 7.0.2 @@ -6299,12 +6363,14 @@ snapshots: '@exodus/bytes@1.15.1': {} - '@google/genai@1.52.0': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': dependencies: google-auth-library: 10.7.0 p-retry: 4.6.2 protobufjs: 7.6.4 ws: 8.21.0 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) transitivePeerDependencies: - bufferutil - supports-color @@ -6563,6 +6629,9 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.20.0': optional: true + '@pkgjs/parseargs@0.11.0': + optional: true + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -8034,6 +8103,8 @@ snapshots: jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 jiti@2.7.0: {} diff --git a/python/sdk-runtime/package.json b/python/sdk-runtime/package.json index 7fba79083a..ab651d0dfd 100644 --- a/python/sdk-runtime/package.json +++ b/python/sdk-runtime/package.json @@ -10,6 +10,7 @@ "@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/gen-cordis-catalog.ts b/scripts/gen-cordis-catalog.ts index be265b1014..fcee663b13 100644 --- a/scripts/gen-cordis-catalog.ts +++ b/scripts/gen-cordis-catalog.ts @@ -26,6 +26,8 @@ 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', @@ -84,13 +86,13 @@ interface ServiceEntry { key: string /** The service class/interface name, e.g. `LlmService`. */ type: string - /** Whether the service class is abstract (a seam interface). */ + /** Whether the service declaration is abstract (a seam interface). */ abstract: boolean - /** Class-level JSDoc prose, one line per paragraph. */ + /** Declaration-level JSDoc prose, one line per paragraph. */ doc: string /** Public method signatures (bodies stripped), in source order. */ methods: string[] - /** Source pointer of the class declaration. */ + /** Source pointer of the service declaration. */ source: string } @@ -172,8 +174,8 @@ export function collectEvents(scanRoot: string = root): EventEntry[] { return entries } -/** Walk every harness `interface Context` block + its service class, hard- - * erroring (aggregated) on any JSDoc-completeness violation: a class or public +/** Walk every harness `interface Context` block + its service declaration, + * hard-erroring (aggregated) on any JSDoc-completeness violation: a service 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. @@ -199,18 +201,23 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] { } } if (keyToType.size === 0) continue - // Find each service class declared in the same file and emit an entry. + // Find each service declaration in the same file and emit an entry. for (const [key, type] of keyToType) { - const cls = sf.statements.find( - (s): s is ts.ClassDeclaration => ts.isClassDeclaration(s) && s.name?.text === type, + const declaration = sf.statements.find( + (s): s is ts.ClassDeclaration | ts.InterfaceDeclaration => + (ts.isClassDeclaration(s) || ts.isInterfaceDeclaration(s)) && s.name?.text === type, ) - if (!cls) continue // a Pick-mixin member (e.g. timer helpers), not a class here - const abstract = cls.modifiers?.some(m => m.kind === ts.SyntaxKind.AbstractKeyword) ?? false - const clsDoc = parseJsDoc(rawJsDoc(text, cls)).doc - if (!clsDoc) violations.push(`service ctx.${key} (${pointer(rel, sf, cls)}): class ${type} has no JSDoc.`) + if (!declaration) continue // a Pick-mixin member (e.g. timer helpers), not a declaration here + const abstract = ts.isInterfaceDeclaration(declaration) + || (declaration.modifiers?.some(m => m.kind === ts.SyntaxKind.AbstractKeyword) ?? false) + const declarationDoc = parseJsDoc(rawJsDoc(text, declaration)).doc + if (!declarationDoc) { + const kind = ts.isInterfaceDeclaration(declaration) ? 'interface' : 'class' + violations.push(`service ctx.${key} (${pointer(rel, sf, declaration)}): ${kind} ${type} has no JSDoc.`) + } const methods: string[] = [] - for (const member of cls.members) { - if (!ts.isMethodDeclaration(member)) continue + for (const member of declaration.members) { + if (!ts.isMethodDeclaration(member) && !ts.isMethodSignature(member)) continue // Only instance methods callable through `ctx.` are surface; // private, protected, and static methods are not. const nonPublic = member.modifiers?.some(m => @@ -238,9 +245,9 @@ export function collectServices(scanRoot: string = root): ServiceEntry[] { key, type, abstract, - doc: clsDoc, + doc: declarationDoc, methods, - source: pointer(rel, sf, cls), + source: pointer(rel, sf, declaration), }) } } diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts index 31df485e1d..ad131c93ba 100644 --- a/scripts/gen-doc-graphs.ts +++ b/scripts/gen-doc-graphs.ts @@ -152,6 +152,14 @@ const SERVICE_ROLES: ServiceRole[] = [ 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.', + }, { key: 'agentLoop', pkg: 'agent-loop', diff --git a/scripts/type-equiv.manifest.json b/scripts/type-equiv.manifest.json index c3e4533ba5..221372aa5c 100644 --- a/scripts/type-equiv.manifest.json +++ b/scripts/type-equiv.manifest.json @@ -11,6 +11,8 @@ { "doc": "docs/core-data-structures/core.md", "symbol": "LlmCallConfig", "source": "packages/llm/llm/src/call-config.ts" }, { "doc": "docs/core-data-structures/core.md", "symbol": "SessionEvent", "source": "packages/core/session/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 8220324210..6a35a3caf3 100644 --- a/scripts/verify-package-readme-model-experience.ts +++ b/scripts/verify-package-readme-model-experience.ts @@ -28,6 +28,7 @@ interface SentenceContract { * so an absent section cannot be mistaken for forgotten documentation. */ const NO_MODEL_EXPERIENCE_SECTION: Readonly> = { + 'packages/core/agent-execution': 'The package is model-agnostic ambient control infrastructure; model-facing consumers own any resulting request surface.', '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.', } diff --git a/tsconfig.build.json b/tsconfig.build.json index 3a57169005..ce7403242c 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -21,6 +21,7 @@ { "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 6585a987d6..34e9028918 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,6 +32,7 @@ { "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" },