From 97606667aa28ba1ccc8e6bbd88768d18fa96decb Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 14 Oct 2026 14:48:43 -0700 Subject: [PATCH] refactor(agents): one toolset source, one nudge runtime, one lifecycle projection The ten workspace toolset files were literal copies of the canonical tool-sets constants; they import them now. The agents' agent.ts wrappers defined zero-importer factories that the runtime never consumed (definitions are built in assemble-runtime / run.ts through @intx/agent); they are gone along with their re-exports and the stale allowlist rows for those wrappers. ChatDirector and SubAgentDirector share a single director-core nudge module instead of carrying two withEphemeralNudge implementations, stop-policy loses its deprecated input shadow, and lifecycle state checks fold into one isRunEligible projection. The qa-lead outlier gets the same package-shape drift test as the other directors. The live-tool-dispatch shim stays: the vendored agent still snapshots its tool map at construction, so the shim is load-bearing. Deleting those modules drops the dead-export scan from 1366 to 1358 files, so the fail-closed floor follows. Family tool names, approve_worker_call, auth helpers, and the provider de-dupe stay. Step-grant/workspace-policy separation and per-director prompt data are untouched. --- agents/artist/README.md | 25 +++----- agents/artist/src/agent.ts | 17 ------ agents/artist/src/index.ts | 1 - agents/artist/src/toolset.ts | 18 +----- agents/coder/README.md | 27 ++++----- agents/coder/src/agent.ts | 17 ------ agents/coder/src/index.ts | 1 - agents/coder/src/toolset.ts | 18 +----- agents/designer/README.md | 25 +++----- agents/designer/src/agent.ts | 17 ------ agents/designer/src/index.ts | 1 - agents/designer/src/toolset.ts | 20 +------ agents/dispatch/README.md | 18 +----- agents/dispatch/src/agent.ts | 19 ------ agents/dispatch/src/index.ts | 1 - agents/dispatch/src/toolset.ts | 30 +--------- agents/explorer/README.md | 27 ++++----- agents/explorer/src/agent.ts | 18 ------ agents/explorer/src/index.ts | 1 - agents/explorer/src/toolset.ts | 18 +----- agents/planner/README.md | 25 +++----- agents/planner/src/agent.ts | 17 ------ agents/planner/src/index.ts | 1 - agents/planner/src/toolset.ts | 18 +----- agents/prober/README.md | 25 +++----- agents/prober/src/agent.ts | 17 ------ agents/prober/src/index.ts | 1 - agents/prober/src/toolset.ts | 18 +----- agents/reviewer/README.md | 25 +++----- agents/reviewer/src/agent.ts | 17 ------ agents/reviewer/src/index.ts | 1 - agents/reviewer/src/toolset.ts | 18 +----- agents/shakespeare/README.md | 25 +++----- agents/shakespeare/src/agent.ts | 17 ------ agents/shakespeare/src/index.ts | 1 - agents/shakespeare/src/toolset.ts | 17 +----- agents/warden/README.md | 16 +---- agents/warden/src/agent.ts | 18 ------ agents/warden/src/index.ts | 1 - agents/warden/src/toolset.ts | 19 +----- scripts/check-dead-exports.test.ts | 6 +- scripts/dead-export-allowlist.txt | 36 ++--------- scripts/dead-export-guard.json | 2 +- src/agent/director-core.ts | 59 ++++++++++++++++++ src/agent/director.ts | 53 ++++------------- src/agent/directors/qa-lead/package.test.ts | 33 +++++++++++ src/subagent/agent-fleet.ts | 10 +--- src/subagent/index.test.ts | 4 +- src/subagent/lifecycle.ts | 8 +++ src/subagent/nudge-director.ts | 66 ++++----------------- src/subagent/session-store.ts | 12 ++-- 51 files changed, 248 insertions(+), 657 deletions(-) delete mode 100644 agents/artist/src/agent.ts delete mode 100644 agents/coder/src/agent.ts delete mode 100644 agents/designer/src/agent.ts delete mode 100644 agents/dispatch/src/agent.ts delete mode 100644 agents/explorer/src/agent.ts delete mode 100644 agents/planner/src/agent.ts delete mode 100644 agents/prober/src/agent.ts delete mode 100644 agents/reviewer/src/agent.ts delete mode 100644 agents/shakespeare/src/agent.ts delete mode 100644 agents/warden/src/agent.ts create mode 100644 src/agent/director-core.ts create mode 100644 src/agent/directors/qa-lead/package.test.ts diff --git a/agents/artist/README.md b/agents/artist/README.md index 220bed49c..917a99345 100644 --- a/agents/artist/README.md +++ b/agents/artist/README.md @@ -3,25 +3,18 @@ ## What this is Ready `@corbits/code-agent-artist` agent + independently importable components: -`agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config`. - -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-artist"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` to a workflow — the package never exports a -workflow itself. +`director`, `tools`, `systemPrompt`, `config`. ## Remix components ```ts -import { director, tools, systemPrompt, config } from "@corbits/code-agent-artist"; -defineAgent({ director, ... }); // or swap tools / systemPrompt / config +import { + director, + tools, + systemPrompt, + config, +} from "@corbits/code-agent-artist"; +// or swap tools / systemPrompt / config ``` Each of `director`, `tools`, `systemPrompt`, and `config` is a named, @@ -30,6 +23,6 @@ independently importable part. ## Single authority `prompt.ts` `systemPrompt.build()` is byte-stable (the raw card, no formatting). -`toolset.ts` `tools` literal equals the `REVIEW_TOOLS` surface constant in +`toolset.ts` `tools` re-exports the `REVIEW_TOOLS` surface constant in `src/agent/directors/tool-sets.ts`; drift is enforced by the in-tree drift-guard `src/agent/directors/artist/package.test.ts`. diff --git a/agents/artist/src/agent.ts b/agents/artist/src/agent.ts deleted file mode 100644 index a150b13c9..000000000 --- a/agents/artist/src/agent.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "artist", - systemPrompt: systemPrompt.build(), - tools: [], - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/artist/src/index.ts b/agents/artist/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/artist/src/index.ts +++ b/agents/artist/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/artist/src/toolset.ts b/agents/artist/src/toolset.ts index d8041a3ef..6bf1c16c9 100644 --- a/agents/artist/src/toolset.ts +++ b/agents/artist/src/toolset.ts @@ -1,15 +1,3 @@ -export const tools: readonly string[] = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", -] as const; +import { REVIEW_TOOLS } from "../../../src/agent/directors/tool-sets.js"; + +export const tools = REVIEW_TOOLS; diff --git a/agents/coder/README.md b/agents/coder/README.md index 73feb562b..766cd6213 100644 --- a/agents/coder/README.md +++ b/agents/coder/README.md @@ -3,35 +3,28 @@ ## What this is Ready `@corbits/code-agent-coder` agent + independently importable components: -`agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config`. Coder is +`director`, `tools`, `systemPrompt`, `config`. Coder is the implementation specialist: minimal safe diffs, root-cause fixes at the proper layer, zero unnecessary abstractions, tests landed with changes, repo check gate. -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-coder"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` to a workflow — the package never exports a -workflow itself. - ## Remix components ```ts -import { director, tools, systemPrompt, config } from "@corbits/code-agent-coder"; -defineAgent({ director, ... }); // or swap tools / systemPrompt / config +import { + director, + tools, + systemPrompt, + config, +} from "@corbits/code-agent-coder"; +// or swap tools / systemPrompt / config ``` -Each of `agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config` is a +Each of `director`, `tools`, `systemPrompt`, `config` is a named, independently importable part. ## Single authority `prompt.ts` `systemPrompt.build()` is byte-stable (the raw card, no formatting). -`toolset.ts` `tools` literal equals the `BUILD_TOOLS` surface constant in +`toolset.ts` `tools` re-exports the `BUILD_TOOLS` surface constant in `src/agent/directors/tool-sets.ts`; drift is enforced by the in-tree drift-guard `src/agent/directors/coder/package.test.ts`. diff --git a/agents/coder/src/agent.ts b/agents/coder/src/agent.ts deleted file mode 100644 index cf7dcc625..000000000 --- a/agents/coder/src/agent.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "coder", - systemPrompt: systemPrompt.build(), - tools: [], // tool mounting is consumer/app-side; surface enforced by drift-guard - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/coder/src/index.ts b/agents/coder/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/coder/src/index.ts +++ b/agents/coder/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/coder/src/toolset.ts b/agents/coder/src/toolset.ts index dc70327e5..51325f077 100644 --- a/agents/coder/src/toolset.ts +++ b/agents/coder/src/toolset.ts @@ -1,17 +1,3 @@ -// BUILD_TOOLS literal; drift-guarded against src/agent/directors/tool-sets.ts. +import { BUILD_TOOLS } from "../../../src/agent/directors/tool-sets.js"; -export const tools = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", -] as const; +export const tools = BUILD_TOOLS; diff --git a/agents/designer/README.md b/agents/designer/README.md index a51f804d0..01a602e8e 100644 --- a/agents/designer/README.md +++ b/agents/designer/README.md @@ -3,25 +3,18 @@ ## What this is Ready `@corbits/code-agent-designer` agent + independently importable components: -`agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config`. - -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-designer"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` to a workflow — the package never exports a -workflow itself. +`director`, `tools`, `systemPrompt`, `config`. ## Remix components ```ts -import { director, tools, systemPrompt, config } from "@corbits/code-agent-designer"; -defineAgent({ director, ... }); // or swap tools / systemPrompt / config +import { + director, + tools, + systemPrompt, + config, +} from "@corbits/code-agent-designer"; +// or swap tools / systemPrompt / config ``` Each of `director`, `tools`, `systemPrompt`, and `config` is a named, @@ -30,6 +23,6 @@ independently importable part. ## Single authority `prompt.ts` `systemPrompt.build()` is byte-stable (the raw card, no formatting). -`toolset.ts` `tools` literal equals the `REVIEW_TOOLS` surface constant in +`toolset.ts` `tools` re-exports the `REVIEW_TOOLS` surface constant in `src/agent/directors/tool-sets.ts`; drift is enforced by the in-tree drift-guard `src/agent/directors/designer/package.test.ts`. diff --git a/agents/designer/src/agent.ts b/agents/designer/src/agent.ts deleted file mode 100644 index 186d24e33..000000000 --- a/agents/designer/src/agent.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "designer", - systemPrompt: systemPrompt.build(), - tools: [], // tool mounting is consumer/app-side; surface enforced by drift-guard - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/designer/src/index.ts b/agents/designer/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/designer/src/index.ts +++ b/agents/designer/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/designer/src/toolset.ts b/agents/designer/src/toolset.ts index 03d33ce03..6bf1c16c9 100644 --- a/agents/designer/src/toolset.ts +++ b/agents/designer/src/toolset.ts @@ -1,17 +1,3 @@ -// BUILD_TOOLS literal (= REVIEW_TOOLS); drift-guarded against REVIEW_TOOLS -// by src/agent/directors/designer/package.test.ts. -export const tools = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", -] as const; +import { REVIEW_TOOLS } from "../../../src/agent/directors/tool-sets.js"; + +export const tools = REVIEW_TOOLS; diff --git a/agents/dispatch/README.md b/agents/dispatch/README.md index 53c4048da..0e8676ef8 100644 --- a/agents/dispatch/README.md +++ b/agents/dispatch/README.md @@ -7,18 +7,6 @@ plus its named component parts so a consumer can recreate or remix it. Dispatch the Tier-1 primary orchestrator: it classifies work, DIYs obvious mechanical corrections, and spawns named specialists. -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-dispatch"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` (`agent`) to a workflow, hub, or sidecar. The -package never exports a workflow itself — a workflow _consumes_ `agent`. - ## Remix components ```ts @@ -27,20 +15,18 @@ import { tools, systemPrompt, config, - defineAgent, } from "@corbits/code-agent-dispatch"; // Swap components at the consumer side. -defineAgent({ director, tools, systemPrompt, config }); // or spread overrides ``` -Each of `agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config` is a +Each of `director`, `tools`, `systemPrompt`, `config` is a named, independently importable part. ## Byte-stability & single authority - `systemPrompt.build()` is pure and byte-stable — identical string every call. -- `tools` is the literal expansion of the app's `DISPATCH_TOOLS` surface constant +- `tools` re-exports the app's `DISPATCH_TOOLS` surface constant (`src/agent/directors/tool-sets.ts`); the in-tree drift-guard (`src/agent/directors/dispatch/package.test.ts`) enforces they match. - This package is the single prompt authority for dispatch; the in-tree diff --git a/agents/dispatch/src/agent.ts b/agents/dispatch/src/agent.ts deleted file mode 100644 index dda56b227..000000000 --- a/agents/dispatch/src/agent.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; - -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -/** Package convenience wrapper → `AgentDefinition` for `runLocal` authoring. */ -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "dispatch", - systemPrompt: systemPrompt.build(), - tools: [], - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/dispatch/src/index.ts b/agents/dispatch/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/dispatch/src/index.ts +++ b/agents/dispatch/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/dispatch/src/toolset.ts b/agents/dispatch/src/toolset.ts index 7ec80a64a..feb55e191 100644 --- a/agents/dispatch/src/toolset.ts +++ b/agents/dispatch/src/toolset.ts @@ -1,27 +1,3 @@ -// Dispatch primary: orchestrator surface plus fleet discovery (Tier-1 only). -// Drift against the app's DISPATCH_TOOLS is enforced by the in-tree -// src/agent/directors/dispatch/package.test.ts. -export const tools = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", - "spawn_agent", - "list_agents", - "close_agent", - "resume_agent", - "interrupt_agent", - "send_input", - "approve_worker_call", - "read_agent_trace", - "search_agents", -] as const; +import { DISPATCH_TOOLS } from "../../../src/agent/directors/tool-sets.js"; + +export const tools = DISPATCH_TOOLS; diff --git a/agents/explorer/README.md b/agents/explorer/README.md index de9c3068f..4424d7463 100644 --- a/agents/explorer/README.md +++ b/agents/explorer/README.md @@ -3,36 +3,29 @@ ## What this is Ready `@corbits/code-agent-explorer` agent + independently importable components: -`agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config`. Explorer is +`director`, `tools`, `systemPrompt`, `config`. Explorer is the read-only mapping & search worker: it maps the codebase against the brief and reports scannable findings — it never implements, reviews, or discovers the fleet. -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-explorer"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` to a workflow — the package never exports a -workflow itself. - ## Remix components ```ts -import { director, tools, systemPrompt, config } from "@corbits/code-agent-explorer"; -defineAgent({ director, ... }); // or swap tools / systemPrompt / config +import { + director, + tools, + systemPrompt, + config, +} from "@corbits/code-agent-explorer"; +// or swap tools / systemPrompt / config ``` -Each of `agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config` is a +Each of `director`, `tools`, `systemPrompt`, `config` is a named, independently importable part. ## Single authority `prompt.ts` `systemPrompt.build()` is byte-stable (the raw card, no formatting). -`toolset.ts` `tools` literal equals the `READ_TOOLS` surface constant in +`toolset.ts` `tools` re-exports the `READ_TOOLS` surface constant in `src/agent/directors/tool-sets.ts` (explorer is deliberately read-only — no `write_file`/`edit_file`/`delete_file`); drift is enforced by the in-tree drift-guard `src/agent/directors/explorer/package.test.ts`. diff --git a/agents/explorer/src/agent.ts b/agents/explorer/src/agent.ts deleted file mode 100644 index 22af654a1..000000000 --- a/agents/explorer/src/agent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; - -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "explorer", - systemPrompt: systemPrompt.build(), - tools: [], - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/explorer/src/index.ts b/agents/explorer/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/explorer/src/index.ts +++ b/agents/explorer/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/explorer/src/toolset.ts b/agents/explorer/src/toolset.ts index 9666634ab..09eaf868e 100644 --- a/agents/explorer/src/toolset.ts +++ b/agents/explorer/src/toolset.ts @@ -1,15 +1,3 @@ -// Explorer read surface only: literal expansion of the `READ_TOOLS` constant in -// src/agent/directors/tool-sets.ts (the single tool authority). Drift is enforced -// in-tree by src/agent/directors/explorer/package.test.ts. -export const tools = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", -] as const; +import { READ_TOOLS } from "../../../src/agent/directors/tool-sets.js"; + +export const tools = READ_TOOLS; diff --git a/agents/planner/README.md b/agents/planner/README.md index 0f4086998..86d0d0d68 100644 --- a/agents/planner/README.md +++ b/agents/planner/README.md @@ -3,25 +3,18 @@ ## What this is Ready `@corbits/code-agent-planner` agent + independently importable components: -`agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config`. - -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-planner"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` to a workflow — the package never exports a -workflow itself. +`director`, `tools`, `systemPrompt`, `config`. ## Remix components ```ts -import { director, tools, systemPrompt, config } from "@corbits/code-agent-planner"; -defineAgent({ director, ... }); // or swap tools / systemPrompt / config +import { + director, + tools, + systemPrompt, + config, +} from "@corbits/code-agent-planner"; +// or swap tools / systemPrompt / config ``` Each of `director`, `tools`, `systemPrompt`, and `config` is a named, @@ -30,6 +23,6 @@ independently importable part. ## Single authority `prompt.ts` `systemPrompt.build()` is byte-stable (the raw card, no formatting). -`toolset.ts` `tools` literal equals the `REVIEW_TOOLS` surface constant in +`toolset.ts` `tools` re-exports the `REVIEW_TOOLS` surface constant in `src/agent/directors/tool-sets.ts`; drift is enforced by the in-tree drift-guard `src/agent/directors/planner/package.test.ts`. diff --git a/agents/planner/src/agent.ts b/agents/planner/src/agent.ts deleted file mode 100644 index bf6cf1bfc..000000000 --- a/agents/planner/src/agent.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "planner", - systemPrompt: systemPrompt.build(), - tools: [], - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/planner/src/index.ts b/agents/planner/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/planner/src/index.ts +++ b/agents/planner/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/planner/src/toolset.ts b/agents/planner/src/toolset.ts index d8041a3ef..6bf1c16c9 100644 --- a/agents/planner/src/toolset.ts +++ b/agents/planner/src/toolset.ts @@ -1,15 +1,3 @@ -export const tools: readonly string[] = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", -] as const; +import { REVIEW_TOOLS } from "../../../src/agent/directors/tool-sets.js"; + +export const tools = REVIEW_TOOLS; diff --git a/agents/prober/README.md b/agents/prober/README.md index 84eee3bdc..1db6261f9 100644 --- a/agents/prober/README.md +++ b/agents/prober/README.md @@ -3,25 +3,18 @@ ## What this is Ready `@corbits/code-agent-prober` agent + independently importable components: -`agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config`. - -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-prober"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` to a workflow — the package never exports a -workflow itself. +`director`, `tools`, `systemPrompt`, `config`. ## Remix components ```ts -import { director, tools, systemPrompt, config } from "@corbits/code-agent-prober"; -defineAgent({ director, ... }); // or swap tools / systemPrompt / config +import { + director, + tools, + systemPrompt, + config, +} from "@corbits/code-agent-prober"; +// or swap tools / systemPrompt / config ``` Each of `director`, `tools`, `systemPrompt`, and `config` is a named, @@ -30,6 +23,6 @@ independently importable part. ## Single authority `prompt.ts` `systemPrompt.build()` is byte-stable (the raw card, no formatting). -`toolset.ts` `tools` literal equals the `REVIEW_TOOLS` surface constant in +`toolset.ts` `tools` re-exports the `REVIEW_TOOLS` surface constant in `src/agent/directors/tool-sets.ts`; drift is enforced by the in-tree drift-guard `src/agent/directors/prober/package.test.ts`. diff --git a/agents/prober/src/agent.ts b/agents/prober/src/agent.ts deleted file mode 100644 index b9e59f6e4..000000000 --- a/agents/prober/src/agent.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "prober", - systemPrompt: systemPrompt.build(), - tools: [], - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/prober/src/index.ts b/agents/prober/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/prober/src/index.ts +++ b/agents/prober/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/prober/src/toolset.ts b/agents/prober/src/toolset.ts index d8041a3ef..6bf1c16c9 100644 --- a/agents/prober/src/toolset.ts +++ b/agents/prober/src/toolset.ts @@ -1,15 +1,3 @@ -export const tools: readonly string[] = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", -] as const; +import { REVIEW_TOOLS } from "../../../src/agent/directors/tool-sets.js"; + +export const tools = REVIEW_TOOLS; diff --git a/agents/reviewer/README.md b/agents/reviewer/README.md index 77d137278..cfd09f6d8 100644 --- a/agents/reviewer/README.md +++ b/agents/reviewer/README.md @@ -3,25 +3,18 @@ ## What this is Ready `@corbits/code-agent-reviewer` agent + independently importable components: -`agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config`. - -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-reviewer"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` to a workflow — the package never exports a -workflow itself. +`director`, `tools`, `systemPrompt`, `config`. ## Remix components ```ts -import { director, tools, systemPrompt, config } from "@corbits/code-agent-reviewer"; -defineAgent({ director, ... }); // or swap tools / systemPrompt / config +import { + director, + tools, + systemPrompt, + config, +} from "@corbits/code-agent-reviewer"; +// or swap tools / systemPrompt / config ``` Each of `director`, `tools`, `systemPrompt`, and `config` is a named, @@ -30,6 +23,6 @@ independently importable part. ## Single authority `prompt.ts` `systemPrompt.build()` is byte-stable (the raw card, no formatting). -`toolset.ts` `tools` literal equals the `REVIEW_TOOLS` surface constant in +`toolset.ts` `tools` re-exports the `REVIEW_TOOLS` surface constant in `src/agent/directors/tool-sets.ts`; drift is enforced by the in-tree drift-guard `src/agent/directors/reviewer/package.test.ts`. diff --git a/agents/reviewer/src/agent.ts b/agents/reviewer/src/agent.ts deleted file mode 100644 index 722791427..000000000 --- a/agents/reviewer/src/agent.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "reviewer", - systemPrompt: systemPrompt.build(), - tools: [], // tool mounting is consumer/app-side; surface enforced by drift-guard - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/reviewer/src/index.ts b/agents/reviewer/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/reviewer/src/index.ts +++ b/agents/reviewer/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/reviewer/src/toolset.ts b/agents/reviewer/src/toolset.ts index 182c2d6c2..6bf1c16c9 100644 --- a/agents/reviewer/src/toolset.ts +++ b/agents/reviewer/src/toolset.ts @@ -1,17 +1,3 @@ -// REVIEWER_TOOLS literal; drift-guarded against src/agent/directors/tool-sets.ts. +import { REVIEW_TOOLS } from "../../../src/agent/directors/tool-sets.js"; -export const tools: readonly string[] = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", -] as const; +export const tools = REVIEW_TOOLS; diff --git a/agents/shakespeare/README.md b/agents/shakespeare/README.md index e8596c7b8..0987abd0f 100644 --- a/agents/shakespeare/README.md +++ b/agents/shakespeare/README.md @@ -3,25 +3,18 @@ ## What this is Ready `@corbits/code-agent-shakespeare` agent + independently importable components: -`agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config`. - -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-shakespeare"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` to a workflow — the package never exports a -workflow itself. +`director`, `tools`, `systemPrompt`, `config`. ## Remix components ```ts -import { director, tools, systemPrompt, config } from "@corbits/code-agent-shakespeare"; -defineAgent({ director, ... }); // or swap tools / systemPrompt / config +import { + director, + tools, + systemPrompt, + config, +} from "@corbits/code-agent-shakespeare"; +// or swap tools / systemPrompt / config ``` Each of `director`, `tools`, `systemPrompt`, and `config` is a named, @@ -30,6 +23,6 @@ independently importable part. ## Single authority `prompt.ts` `systemPrompt.build()` is byte-stable (the raw card, no formatting). -`toolset.ts` `tools` literal equals the `DOCS_TOOLS` surface constant in +`toolset.ts` `tools` re-exports the `DOCS_TOOLS` surface constant in `src/agent/directors/tool-sets.ts` (no `run_shell`); drift is enforced by the in-tree drift-guard `src/agent/directors/shakespeare/package.test.ts`. diff --git a/agents/shakespeare/src/agent.ts b/agents/shakespeare/src/agent.ts deleted file mode 100644 index 6cfe96ffd..000000000 --- a/agents/shakespeare/src/agent.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "shakespeare", - systemPrompt: systemPrompt.build(), - tools: [], - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/shakespeare/src/index.ts b/agents/shakespeare/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/shakespeare/src/index.ts +++ b/agents/shakespeare/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/shakespeare/src/toolset.ts b/agents/shakespeare/src/toolset.ts index f221dbaf9..f8687272c 100644 --- a/agents/shakespeare/src/toolset.ts +++ b/agents/shakespeare/src/toolset.ts @@ -1,14 +1,3 @@ -export const tools: readonly string[] = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", -] as const; +import { DOCS_TOOLS } from "../../../src/agent/directors/tool-sets.js"; + +export const tools = DOCS_TOOLS; diff --git a/agents/warden/README.md b/agents/warden/README.md index cc69c882e..13a5ecd02 100644 --- a/agents/warden/README.md +++ b/agents/warden/README.md @@ -7,18 +7,6 @@ plus its named component parts so a consumer can recreate or remix it. Warden is the trust-review worker: it reviews permission, provider-auth, and plugin-loader diffs only, and never fixes product code. -## Consume (runLocal) - -```ts -import { agent } from "@corbits/code-agent-warden"; -const run = runLocal(defining(agent), env); -``` - -## Consume (deploy runWorkflow / hub / sidecar) - -Hand the same `AgentDefinition` (`agent`) to a workflow, hub, or sidecar. The -package never exports a workflow itself — a workflow _consumes_ `agent`. - ## Remix components ```ts @@ -27,14 +15,12 @@ import { tools, systemPrompt, config, - defineAgent, } from "@corbits/code-agent-warden"; // Swap components at the consumer side. -defineAgent({ director, tools, systemPrompt, config }); // or spread overrides ``` -Each of `agent`, `defineAgent`, `director`, `tools`, `systemPrompt`, `config` is a +Each of `director`, `tools`, `systemPrompt`, `config` is a named, independently importable part. ## Byte-stability & single authority diff --git a/agents/warden/src/agent.ts b/agents/warden/src/agent.ts deleted file mode 100644 index f0bc34894..000000000 --- a/agents/warden/src/agent.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - defineAgent as coreDefineAgent, - type AgentDefinition, -} from "@intx/agent"; - -import { systemPrompt } from "./prompt.js"; - -export const agent: AgentDefinition = defineAgent(); - -export function defineAgent(): AgentDefinition { - return coreDefineAgent({ - id: "warden", - systemPrompt: systemPrompt.build(), - tools: [], - capabilities: [], - inference: { sources: [] }, - }); -} diff --git a/agents/warden/src/index.ts b/agents/warden/src/index.ts index c2c15f6c6..adb90d291 100644 --- a/agents/warden/src/index.ts +++ b/agents/warden/src/index.ts @@ -1,4 +1,3 @@ -export { agent, defineAgent } from "./agent.js"; export { director } from "./director.js"; export { tools } from "./toolset.js"; export { systemPrompt } from "./prompt.js"; diff --git a/agents/warden/src/toolset.ts b/agents/warden/src/toolset.ts index 4a230a9af..6bf1c16c9 100644 --- a/agents/warden/src/toolset.ts +++ b/agents/warden/src/toolset.ts @@ -1,16 +1,3 @@ -// REVIEW_TOOLS literal; drift-guarded against src/agent/directors/tool-sets.ts. -export const tools = [ - "read_file", - "grep", - "search_files", - "list_dir", - "lsp", - "run_shell", - "web_fetch", - "web_search", - "skill_search", - "use_skill", - "write_file", - "edit_file", - "delete_file", -] as const; +import { REVIEW_TOOLS } from "../../../src/agent/directors/tool-sets.js"; + +export const tools = REVIEW_TOOLS; diff --git a/scripts/check-dead-exports.test.ts b/scripts/check-dead-exports.test.ts index 860d4dfc5..7fe4a8ece 100644 --- a/scripts/check-dead-exports.test.ts +++ b/scripts/check-dead-exports.test.ts @@ -280,9 +280,9 @@ describe("live knip scan semantics", () => { // The re-added allowlist entries match every surfaced finding. expect(isAllowlisted(rules, index, "tools")).toBe(true); expect(isAllowlisted(rules, index, "config")).toBe(true); - expect(isAllowlisted(rules, `${config}/src/agent.ts`, "agent")).toBe( - true, - ); + // The agent/defineAgent wrappers were deleted with their module + // (CL-10669): no production importer existed, and the surface no + // longer carries them in either scan. } expect(reported("agents/dispatch/src/index.ts", "systemPrompt")).toBe( false, diff --git a/scripts/dead-export-allowlist.txt b/scripts/dead-export-allowlist.txt index 096ff04f9..9f1a1051d 100644 --- a/scripts/dead-export-allowlist.txt +++ b/scripts/dead-export-allowlist.txt @@ -14,64 +14,36 @@ # Public contract of each private @corbits/code-agent-* package (agent-template # lane): the custom-agent authoring surface. DIRECTOR_REGISTRY consumes # `director`, and src/agent/prompts.ts consumes dispatch's `systemPrompt`, so -# those names are not listed. `agent` in `agent.ts` is the definition the -# package entry re-exports. -agents/dispatch/src/index.ts: agent -agents/dispatch/src/index.ts: defineAgent +# those names are not listed. The agent/defineAgent wrappers were deleted with +# their module (CL-10669): no production importer existed. The default scan +# counts tools/systemPrompt/config alive through each package's contract test, +# so they resurface only under the production agent-workspace scan. agents/dispatch/src/index.ts: tools agents/dispatch/src/index.ts: config -agents/dispatch/src/agent.ts: agent -agents/warden/src/index.ts: agent -agents/warden/src/index.ts: defineAgent agents/warden/src/index.ts: tools agents/warden/src/index.ts: systemPrompt agents/warden/src/index.ts: config -agents/warden/src/agent.ts: agent -agents/designer/src/index.ts: agent -agents/designer/src/index.ts: defineAgent agents/designer/src/index.ts: tools agents/designer/src/index.ts: systemPrompt agents/designer/src/index.ts: config -agents/designer/src/agent.ts: agent -agents/reviewer/src/index.ts: agent -agents/reviewer/src/index.ts: defineAgent agents/reviewer/src/index.ts: tools agents/reviewer/src/index.ts: systemPrompt agents/reviewer/src/index.ts: config -agents/reviewer/src/agent.ts: agent -agents/planner/src/index.ts: agent -agents/planner/src/index.ts: defineAgent agents/planner/src/index.ts: tools agents/planner/src/index.ts: systemPrompt agents/planner/src/index.ts: config -agents/planner/src/agent.ts: agent -agents/coder/src/index.ts: agent -agents/coder/src/index.ts: defineAgent agents/coder/src/index.ts: tools agents/coder/src/index.ts: systemPrompt agents/coder/src/index.ts: config -agents/coder/src/agent.ts: agent -agents/explorer/src/index.ts: agent -agents/explorer/src/index.ts: defineAgent agents/explorer/src/index.ts: tools agents/explorer/src/index.ts: systemPrompt agents/explorer/src/index.ts: config -agents/explorer/src/agent.ts: agent -agents/artist/src/index.ts: agent -agents/artist/src/index.ts: defineAgent agents/artist/src/index.ts: tools agents/artist/src/index.ts: systemPrompt agents/artist/src/index.ts: config -agents/artist/src/agent.ts: agent -agents/shakespeare/src/index.ts: agent -agents/shakespeare/src/index.ts: defineAgent agents/shakespeare/src/index.ts: tools agents/shakespeare/src/index.ts: systemPrompt agents/shakespeare/src/index.ts: config -agents/shakespeare/src/agent.ts: agent -agents/prober/src/index.ts: agent -agents/prober/src/index.ts: defineAgent agents/prober/src/index.ts: tools agents/prober/src/index.ts: systemPrompt agents/prober/src/index.ts: config -agents/prober/src/agent.ts: agent diff --git a/scripts/dead-export-guard.json b/scripts/dead-export-guard.json index 5e8d9b75b..b5202ce61 100644 --- a/scripts/dead-export-guard.json +++ b/scripts/dead-export-guard.json @@ -1,4 +1,4 @@ { "tsconfig": "tsconfig.json", - "minScannedFiles": 1366 + "minScannedFiles": 1358 } diff --git a/src/agent/director-core.ts b/src/agent/director-core.ts new file mode 100644 index 000000000..c2ae14fdb --- /dev/null +++ b/src/agent/director-core.ts @@ -0,0 +1,59 @@ +/** + * Shared director runtime primitives for the two Corbits directors + * (ChatDirector and SubAgentDirector). Both extend the vendored + * DefaultDirector; their duplicate nudge-turn plumbing lives here so the + * ephemeral-nudge wire shape has one definition. + */ + +import type { + ReactorCapabilities, + ReactorAction, + ConversationTurn, +} from "@intx/types/runtime"; +import type { ExtendedInferenceOptions } from "@intx/inference"; + +/** + * Tail-appended discipline rules keep the prompt-cache hit; the base director + * composes before super() for the same reason. + */ +export function composeDirectedPrompt( + systemPrompt: string, + disciplineRules: string | undefined, +): string { + return disciplineRules !== undefined && disciplineRules.length > 0 + ? `${systemPrompt}\n\n${disciplineRules}` + : systemPrompt; +} + +function ephemeralNudgeTurn(text: string): ConversationTurn { + return { + role: "user", + content: [{ type: "text", text: text.trim() }], + timestamp: Date.now(), + }; +} + +/** + * Attach an armed nudge to an existing infer instead of building a fresh one + * — a tool_use turn must be followed by tool_result, never a bare user turn, + * so the nudge rides the infer that follows the pending tool calls. + */ +export function withEphemeralNudge( + options: ExtendedInferenceOptions | undefined, + text: string, +): ExtendedInferenceOptions { + const turn = ephemeralNudgeTurn(text); + const existing = options?.ephemeralTurns; + if (existing === undefined || existing.length === 0) { + return { ...(options ?? {}), ephemeralTurns: [turn] }; + } + return { ...(options ?? {}), ephemeralTurns: [...existing, turn] }; +} + +export function inferWithEphemeralNudge( + capabilities: ReactorCapabilities, + text: string, + options?: ExtendedInferenceOptions, +): ReactorAction { + return capabilities.infer(withEphemeralNudge(options ?? {}, text)); +} diff --git a/src/agent/director.ts b/src/agent/director.ts index 42db026bd..49fa51946 100644 --- a/src/agent/director.ts +++ b/src/agent/director.ts @@ -1,8 +1,5 @@ import { isSameTool } from "./canonical-tool-name.js"; -import { - DefaultDirector, - type ExtendedInferenceOptions, -} from "@intx/inference"; +import { DefaultDirector } from "@intx/inference"; import { createHash } from "node:crypto"; import { getLogger } from "@intx/log"; import type { @@ -31,6 +28,11 @@ import { onTurnBoundary } from "./reactor-events.js"; import { isOperatorOriginated } from "./message-provenance.js"; import { errorMessage } from "./error-message.js"; import { type } from "arktype"; +import { + composeDirectedPrompt, + inferWithEphemeralNudge, + withEphemeralNudge, +} from "./director-core.js"; import { applyManageTasks, hasActiveTasks, @@ -75,34 +77,6 @@ function isInternalRecoveryAbort( return isInternalRecoveryAbortRaw(event.error.raw); } -function directorNudgeTurn(text: string): ConversationTurn { - return { - role: "user", - content: [{ type: "text", text: text.trim() }], - timestamp: Date.now(), - }; -} - -function withEphemeralNudge( - options: ExtendedInferenceOptions, - nudge: string, -): ExtendedInferenceOptions { - const turn = directorNudgeTurn(nudge); - const existing = options.ephemeralTurns; - if (existing === undefined || existing.length === 0) { - return { ...options, ephemeralTurns: [turn] }; - } - return { ...options, ephemeralTurns: [...existing, turn] }; -} - -function inferWithNudge( - capabilities: ReactorCapabilities, - nudge: string, - options?: ExtendedInferenceOptions, -): ReactorAction { - return capabilities.infer(withEphemeralNudge(options ?? {}, nudge)); -} - // agent.send() resolves only on reply (or fatal shutdown): a terminal bare // wait hangs the send and the TUI spinner. A bare wait only arises on an // empty model turn, so settle it with an empty reply (no transcript block); @@ -536,10 +510,7 @@ class ChatDirectorImpl extends DefaultDirector { const disciplineRules = familyPolicy.toolDisciplineRules; // Tool-discipline rules go at the tail: tail appends keep the prompt- // cache hit (99.1%) while a head edit drops it to 9%. - const composedPrompt = - disciplineRules !== undefined && disciplineRules.length > 0 - ? `${systemPrompt}\n\n${disciplineRules}` - : systemPrompt; + const composedPrompt = composeDirectedPrompt(systemPrompt, disciplineRules); super(composedPrompt, toolDefinitions, {}); this._systemPrompt = composedPrompt; this._toolDefinitions = toolDefinitions; @@ -809,7 +780,7 @@ class ChatDirectorImpl extends DefaultDirector { ReactorAction, { type: "infer" } >; - rewritten[inferIndex] = inferWithNudge( + rewritten[inferIndex] = inferWithEphemeralNudge( capabilities, this.modelFamilyPolicy.wrapUpNudgeText, existing.options, @@ -1133,7 +1104,7 @@ class ChatDirectorImpl extends DefaultDirector { this.declinedTerminationNudges++; return [ capabilities.checkpoint("operator-declined"), - inferWithNudge(capabilities, DECLINED_OPEN_TASK_NUDGE), + inferWithEphemeralNudge(capabilities, DECLINED_OPEN_TASK_NUDGE), ]; } this.logTerminationWithOpenTasks("operator-declined"); @@ -1176,7 +1147,7 @@ class ChatDirectorImpl extends DefaultDirector { ) { if (this.spacerEchoNudges < MAX_SPACER_ECHO_NUDGES) { this.spacerEchoNudges++; - return inferWithNudge(capabilities, SPACER_ECHO_NUDGE); + return inferWithEphemeralNudge(capabilities, SPACER_ECHO_NUDGE); } } @@ -1258,7 +1229,7 @@ class ChatDirectorImpl extends DefaultDirector { { type: "wait" } | { type: "reply" } > => a.type !== "wait" && a.type !== "reply", ); - return [...passThrough, inferWithNudge(capabilities, nudge)]; + return [...passThrough, inferWithEphemeralNudge(capabilities, nudge)]; } } } @@ -1293,7 +1264,7 @@ class ChatDirectorImpl extends DefaultDirector { const nudge = coordinatorActive ? WORKFLOW_OPEN_TASK_NUDGE : IDLE_OPEN_TASK_NUDGE; - return [...passThrough, inferWithNudge(capabilities, nudge)]; + return [...passThrough, inferWithEphemeralNudge(capabilities, nudge)]; } this.logTerminationWithOpenTasks("idle-stall"); } diff --git a/src/agent/directors/qa-lead/package.test.ts b/src/agent/directors/qa-lead/package.test.ts new file mode 100644 index 000000000..e16135319 --- /dev/null +++ b/src/agent/directors/qa-lead/package.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "bun:test"; + +import { REVIEW_TOOLS } from "../tool-sets.js"; +import { DIRECTOR_REGISTRY } from "../registry.js"; +import { qaLeadPackage } from "./package.js"; +import type { DirectorPackage } from "../types.js"; +import { defined } from "../../../../testkit/defined.js"; + +describe("qaLeadPackage", () => { + test("package shape satisfies the director contract and feeds the registry", () => { + const asDirector: DirectorPackage = qaLeadPackage; + expect(asDirector.id).toBe("qa-lead"); + expect(DIRECTOR_REGISTRY["qa-lead"]).toBe(qaLeadPackage); + }); + + test("tools surface matches REVIEW_TOOLS and is leaf-scoped", () => { + const allow = defined(qaLeadPackage.tools?.allow); + expect([...allow]).toEqual([...REVIEW_TOOLS]); + expect(new Set(allow).size).toBe(allow.length); + expect( + (defined(qaLeadPackage.tools) as { deny?: unknown }).deny, + ).toBeUndefined(); + expect(allow).not.toContain("spawn_agent"); + expect(allow).not.toContain("wait_agents"); + expect(allow).not.toContain("search_agents"); + }); + + test("hands-on leaf: no spawn, test model role", () => { + expect(qaLeadPackage.spawn.maySpawn).toBe(false); + expect(qaLeadPackage.tier).toBe("leaf"); + expect(qaLeadPackage.modelRole).toBe("test"); + }); +}); diff --git a/src/subagent/agent-fleet.ts b/src/subagent/agent-fleet.ts index a2f2c1ec0..33ca498af 100644 --- a/src/subagent/agent-fleet.ts +++ b/src/subagent/agent-fleet.ts @@ -75,6 +75,7 @@ import { } from "./session-store.js"; import { isLiveWaitStatus, + isRunEligible, projectWaitStatus, type WaitJSONStatus, } from "./lifecycle.js"; @@ -1035,11 +1036,7 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { }; const isOverlapLive = (id: string): boolean => { const session = deps.sessions.get(id); - return ( - session !== undefined && - (session.lifecycle.state === "pending_init" || - session.lifecycle.state === "running") - ); + return session !== undefined && isRunEligible(session.lifecycle); }; const isDeclaredReadOnly = (modelRole: ModelRole | undefined): boolean => modelRole === "explore" || @@ -1520,9 +1517,8 @@ export function createSpawnAgentTool(deps: AgentFleetDeps): AgentTool { const stillAdmissible = (): boolean => { const live = deps.sessions.get(session.id); if (live === undefined) return false; - const state = live.lifecycle.state; // Interrupted lingers on the strip (isLiveStrip) but must not start run(). - return state === "pending_init" || state === "running"; + return isRunEligible(live.lifecycle); }; const start = (): void => { diff --git a/src/subagent/index.test.ts b/src/subagent/index.test.ts index e2f9bcfd5..8c9603db8 100644 --- a/src/subagent/index.test.ts +++ b/src/subagent/index.test.ts @@ -342,7 +342,7 @@ describe("sub-agent stop helpers", () => { ).toBe("incomplete-report"); }); - test("evaluateSubAgentStop returns incomplete-report-stop for Summary-only after the wrap-up nudge", () => { + test("evaluateSubAgentStop returns incomplete-report-stop after the wrap-up nudge cycle", () => { expect( evaluateSubAgentStop({ hasToolCalls: false, @@ -363,7 +363,7 @@ describe("sub-agent stop helpers", () => { ).toBe("stop"); }); - test("evaluateSubAgentStop nudges once on the first tool-less turn, then stops at the cap", () => { + test("evaluateSubAgentStop's toolLessNarrationCycles drives the spiral directly", () => { expect( evaluateSubAgentStop({ hasToolCalls: false, diff --git a/src/subagent/lifecycle.ts b/src/subagent/lifecycle.ts index 069a775fb..828767b5b 100644 --- a/src/subagent/lifecycle.ts +++ b/src/subagent/lifecycle.ts @@ -82,6 +82,14 @@ export function isAlreadyClosed(lifecycle: WorkerLifecycle): boolean { return lifecycle.state === "failed" || lifecycle.state === "shutdown"; } +/** + * A run may still be attached: pending_init or running. Interrupted lingers + * as running on the strip (isLiveStrip) but must not start a run. + */ +export function isRunEligible(lifecycle: WorkerLifecycle): boolean { + return lifecycle.state === "pending_init" || lifecycle.state === "running"; +} + export function isResumableLifecycle( retained: boolean | undefined, lifecycle: WorkerLifecycle, diff --git a/src/subagent/nudge-director.ts b/src/subagent/nudge-director.ts index a89f645dc..a90505e28 100644 --- a/src/subagent/nudge-director.ts +++ b/src/subagent/nudge-director.ts @@ -3,17 +3,13 @@ * recovery for quiet leaves. */ -import { - DefaultDirector, - type ExtendedInferenceOptions, -} from "@intx/inference"; +import { DefaultDirector } from "@intx/inference"; import type { ReactorInboundEvent, ReactorState, ReactorCapabilities, ReactorAction, ToolDefinition, - ConversationTurn, RetryPolicy, } from "@intx/types/runtime"; import { @@ -22,6 +18,11 @@ import { } from "../agent/compaction.js"; import { onTurnBoundary } from "../agent/reactor-events.js"; import { createCorbitsRetryPolicy } from "../agent/retry-policy.js"; +import { + composeDirectedPrompt, + inferWithEphemeralNudge, + withEphemeralNudge, +} from "../agent/director-core.js"; import { EMPTY_THRASH_STATE, nextThrashState, @@ -67,52 +68,11 @@ function hasVerbatimToolCallMarkup( ); } -function ephemeralNudgeTurn(text: string): ConversationTurn { - return { - role: "user", - content: [{ type: "text", text }], - timestamp: Date.now(), - }; -} - const SUBAGENT_STALL_NUDGE = "No activity has been observed for a while. If you are waiting on a " + "background command, check its status now; otherwise continue working or " + "write your report."; -function inferWithSubAgentNudge( - capabilities: ReactorCapabilities, - text: string, -): ReactorAction { - const options: ExtendedInferenceOptions = { - ephemeralTurns: [ - { - role: "user", - content: [{ type: "text", text }], - timestamp: Date.now(), - }, - ], - }; - return capabilities.infer(options); -} - -/** - * Attach the armed nudge to an existing infer instead of building a fresh - * one — a tool_use turn must be followed by tool_result, never a bare user - * turn, so the nudge rides the infer that follows the pending tool calls. - */ -function withEphemeralNudge( - options: ExtendedInferenceOptions | undefined, - text: string, -): ExtendedInferenceOptions { - const turn = ephemeralNudgeTurn(text); - const existing = options?.ephemeralTurns; - if (existing === undefined || existing.length === 0) { - return { ...(options ?? {}), ephemeralTurns: [turn] }; - } - return { ...(options ?? {}), ephemeralTurns: [...existing, turn] }; -} - function isEmptyContinuation(event: ReactorInboundEvent): boolean { if (event.type !== "message.received") return false; const content = event.message.content; @@ -240,10 +200,10 @@ export class SubAgentDirector extends DefaultDirector { ) { // Composed before super() like ChatDirectorImpl: the base director sends // its own copy, so anything appended after super() never reaches the wire. - const composedPrompt = - toolDisciplineRules !== undefined && toolDisciplineRules.length > 0 - ? `${systemPrompt}\n\n${toolDisciplineRules}` - : systemPrompt; + const composedPrompt = composeDirectedPrompt( + systemPrompt, + toolDisciplineRules, + ); super(composedPrompt, toolDefinitions, {}); this._systemPrompt = composedPrompt; this.advertisedTools = toolDefinitions; @@ -413,7 +373,7 @@ export class SubAgentDirector extends DefaultDirector { }); return [ capabilities.checkpoint("subagent-verbatim-tool-call-nudge"), - inferWithSubAgentNudge(capabilities, VERBATIM_TOOL_CALL_NUDGE), + inferWithEphemeralNudge(capabilities, VERBATIM_TOOL_CALL_NUDGE), ]; } if (stop === "incomplete-report") { @@ -434,7 +394,7 @@ export class SubAgentDirector extends DefaultDirector { }); return [ capabilities.checkpoint("subagent-incomplete-report-nudge"), - inferWithSubAgentNudge( + inferWithEphemeralNudge( capabilities, stubPlan ? PLAN_SUBSTANCE_NUDGE : INCOMPLETE_REPORT_NUDGE, ), @@ -542,7 +502,7 @@ export class SubAgentDirector extends DefaultDirector { }); return [ capabilities.checkpoint("subagent-stall-nudge"), - inferWithSubAgentNudge(capabilities, SUBAGENT_STALL_NUDGE), + inferWithEphemeralNudge(capabilities, SUBAGENT_STALL_NUDGE), ]; } diff --git a/src/subagent/session-store.ts b/src/subagent/session-store.ts index 5381e5efd..f27284736 100644 --- a/src/subagent/session-store.ts +++ b/src/subagent/session-store.ts @@ -11,6 +11,7 @@ import { isAlreadyClosed, isLiveStrip, isResumableLifecycle, + isRunEligible, projectLifecycleStatus, projectStripStatus, type WorkerLifecycle, @@ -840,8 +841,7 @@ export function createSubAgentSessionStore( // record never coexists with a live lane: an interrupted worker that // resumes starts a new attempt. const reconcileTerminalRecord = (session: StoredSession): void => { - const state = session.lifecycle.state; - if (state === "pending_init" || state === "running") { + if (isRunEligible(session.lifecycle)) { delete session.failure; return; } @@ -892,8 +892,7 @@ export function createSubAgentSessionStore( const enforceSettledRunInvariant = (id: string): void => { const session = sessions.get(id); if (session === undefined || !runInFlight.has(id)) return; - const state = session.lifecycle.state; - if (state === "pending_init" || state === "running") return; + if (isRunEligible(session.lifecycle)) return; if ( interruptHandles.has(id) || closeHandles.has(id) || @@ -991,8 +990,7 @@ export function createSubAgentSessionStore( // "completed" or "interrupted") is governed by pruneRetained's own cap, // not this one. const isOpenRetained = (s: StoredSession): boolean => - s.retained === true && - (s.lifecycle.state === "completed" || s.lifecycle.state === "interrupted"); + isResumableLifecycle(s.retained, s.lifecycle); const isPinned = (id: string): boolean => (pinCounts.get(id) ?? 0) > 0; @@ -2444,7 +2442,7 @@ export function createSubAgentSessionStore( runInFlight.delete(id); return; } - if (state === "pending_init" || state === "running") { + if (isRunEligible(session.lifecycle)) { session.lifecycle = { state: "interrupted", report }; session.report = report; session.finishedAt = session.finishedAt ?? now();