@namzu/cli 18.1.0 → 20.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5951 -0
- package/README.md +23 -24
- package/dist/bin.js +12 -1
- package/dist/bin.js.map +1 -1
- package/dist/checkpoints/store.d.ts +75 -0
- package/dist/checkpoints/store.d.ts.map +1 -0
- package/dist/checkpoints/store.js +177 -0
- package/dist/checkpoints/store.js.map +1 -0
- package/dist/checkpoints/wrap.d.ts +15 -0
- package/dist/checkpoints/wrap.d.ts.map +1 -0
- package/dist/checkpoints/wrap.js +37 -0
- package/dist/checkpoints/wrap.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +2 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +15 -2
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/provider-wait.d.ts +44 -0
- package/dist/commands/provider-wait.d.ts.map +1 -0
- package/dist/commands/provider-wait.js +65 -0
- package/dist/commands/provider-wait.js.map +1 -0
- package/dist/commands/run-flags.d.ts +13 -1
- package/dist/commands/run-flags.d.ts.map +1 -1
- package/dist/commands/run-flags.js +41 -0
- package/dist/commands/run-flags.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +33 -13
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +132 -78
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/stdin.d.ts +17 -0
- package/dist/commands/stdin.d.ts.map +1 -0
- package/dist/commands/stdin.js +69 -0
- package/dist/commands/stdin.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +107 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +80 -5
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/doctrine.d.ts +16 -38
- package/dist/context/doctrine.d.ts.map +1 -1
- package/dist/context/doctrine.js +16 -75
- package/dist/context/doctrine.js.map +1 -1
- package/dist/context/environment.d.ts +2 -0
- package/dist/context/environment.d.ts.map +1 -1
- package/dist/context/environment.js +3 -0
- package/dist/context/environment.js.map +1 -1
- package/dist/integrations/mcp/servers.d.ts +19 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +23 -2
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/plugins/runtime.d.ts +8 -2
- package/dist/integrations/plugins/runtime.d.ts.map +1 -1
- package/dist/integrations/plugins/runtime.js +24 -12
- package/dist/integrations/plugins/runtime.js.map +1 -1
- package/dist/integrations/providers/chain-capabilities.js +1 -1
- package/dist/integrations/providers/chain-capabilities.js.map +1 -1
- package/dist/integrations/providers/credential-store.d.ts +1 -1
- package/dist/integrations/providers/credential-store.js +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +7 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +2 -2
- package/dist/integrations/providers/registry.js +2 -2
- package/dist/integrations/sessions/store.d.ts +14 -22
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +78 -175
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +6 -2
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/state/identity.d.ts +21 -0
- package/dist/integrations/state/identity.d.ts.map +1 -0
- package/dist/integrations/state/identity.js +49 -0
- package/dist/integrations/state/identity.js.map +1 -0
- package/dist/integrations/state/immutable-json.d.ts +10 -0
- package/dist/integrations/state/immutable-json.d.ts.map +1 -0
- package/dist/integrations/state/immutable-json.js +38 -0
- package/dist/integrations/state/immutable-json.js.map +1 -0
- package/dist/integrations/state/project.d.ts +10 -0
- package/dist/integrations/state/project.d.ts.map +1 -0
- package/dist/integrations/state/project.js +15 -0
- package/dist/integrations/state/project.js.map +1 -0
- package/dist/integrations/state/report.d.ts.map +1 -1
- package/dist/integrations/state/report.js +42 -12
- package/dist/integrations/state/report.js.map +1 -1
- package/dist/integrations/subagents/definitions.d.ts +19 -0
- package/dist/integrations/subagents/definitions.d.ts.map +1 -0
- package/dist/integrations/subagents/definitions.js +26 -0
- package/dist/integrations/subagents/definitions.js.map +1 -0
- package/dist/integrations/subagents/parent.d.ts +13 -0
- package/dist/integrations/subagents/parent.d.ts.map +1 -0
- package/dist/integrations/subagents/parent.js +57 -0
- package/dist/integrations/subagents/parent.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +33 -16
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +450 -150
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/memory/presentation.d.ts +9 -0
- package/dist/memory/presentation.d.ts.map +1 -0
- package/dist/memory/presentation.js +37 -0
- package/dist/memory/presentation.js.map +1 -0
- package/dist/memory/store.d.ts +35 -15
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +79 -25
- package/dist/memory/store.js.map +1 -1
- package/dist/permissions/mode.d.ts +15 -45
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +36 -13
- package/dist/permissions/mode.js.map +1 -1
- package/dist/release-notes.d.ts +20 -0
- package/dist/release-notes.d.ts.map +1 -0
- package/dist/release-notes.js +64 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts +1 -1
- package/dist/tui/AgentExplorer.d.ts.map +1 -1
- package/dist/tui/AgentExplorer.js +25 -21
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +701 -245
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/BrandHeader.d.ts +12 -0
- package/dist/tui/BrandHeader.d.ts.map +1 -0
- package/dist/tui/BrandHeader.js +18 -0
- package/dist/tui/BrandHeader.js.map +1 -0
- package/dist/tui/ChoicePicker.d.ts +11 -1
- package/dist/tui/ChoicePicker.d.ts.map +1 -1
- package/dist/tui/ChoicePicker.js +46 -13
- package/dist/tui/ChoicePicker.js.map +1 -1
- package/dist/tui/Composer.d.ts +5 -2
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +6 -11
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/ComposerFrame.d.ts +10 -0
- package/dist/tui/ComposerFrame.d.ts.map +1 -0
- package/dist/tui/ComposerFrame.js +93 -0
- package/dist/tui/ComposerFrame.js.map +1 -0
- package/dist/tui/EditPromptPicker.d.ts.map +1 -1
- package/dist/tui/EditPromptPicker.js +1 -1
- package/dist/tui/EditPromptPicker.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +12 -5
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +17 -32
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +26 -2
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +109 -16
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +2 -2
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +82 -52
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +5 -2
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/TaskList.d.ts +3 -1
- package/dist/tui/TaskList.d.ts.map +1 -1
- package/dist/tui/TaskList.js +13 -4
- package/dist/tui/TaskList.js.map +1 -1
- package/dist/tui/Transcript.d.ts +4 -12
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +45 -75
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +116 -51
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +539 -307
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/choice-selection.d.ts +17 -0
- package/dist/tui/choice-selection.d.ts.map +1 -0
- package/dist/tui/choice-selection.js +38 -0
- package/dist/tui/choice-selection.js.map +1 -0
- package/dist/tui/exit-summary.d.ts +7 -1
- package/dist/tui/exit-summary.d.ts.map +1 -1
- package/dist/tui/exit-summary.js +12 -3
- package/dist/tui/exit-summary.js.map +1 -1
- package/dist/tui/index.d.ts +3 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -3
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/logo.d.ts +7 -22
- package/dist/tui/logo.d.ts.map +1 -1
- package/dist/tui/logo.js +8 -34
- package/dist/tui/logo.js.map +1 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +21 -6
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/permission-review.d.ts +15 -4
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +48 -9
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/provider-selection.d.ts +4 -0
- package/dist/tui/provider-selection.d.ts.map +1 -0
- package/dist/tui/provider-selection.js +16 -0
- package/dist/tui/provider-selection.js.map +1 -0
- package/dist/tui/run-interruption.d.ts +3 -2
- package/dist/tui/run-interruption.d.ts.map +1 -1
- package/dist/tui/run-interruption.js +2 -1
- package/dist/tui/run-interruption.js.map +1 -1
- package/dist/tui/shell-escape.d.ts +42 -0
- package/dist/tui/shell-escape.d.ts.map +1 -0
- package/dist/tui/shell-escape.js +110 -0
- package/dist/tui/shell-escape.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +82 -31
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +518 -306
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/terminal-choice-text.d.ts +8 -0
- package/dist/tui/terminal-choice-text.d.ts.map +1 -0
- package/dist/tui/terminal-choice-text.js +37 -0
- package/dist/tui/terminal-choice-text.js.map +1 -0
- package/dist/tui/theme.d.ts +3 -4
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +16 -17
- package/dist/tui/theme.js.map +1 -1
- package/dist/tui/types.d.ts +9 -13
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +7 -5
|
@@ -3,56 +3,36 @@
|
|
|
3
3
|
* DEFINE a specialist on the fly — pass a `role` (the persona / system prompt)
|
|
4
4
|
* and namzu spins up a fresh sub-agent with that role at runtime, no
|
|
5
5
|
* pre-registered definition needed. Omit `role` for a general-purpose one.
|
|
6
|
-
* The call
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The call waits for a final result unless operator input releases the wait.
|
|
7
|
+
* The child then stays owned by the parent run, and its completion reaches the
|
|
8
|
+
* same query's inbox. Headless callers without an input waiter remain blocking.
|
|
9
9
|
*
|
|
10
10
|
* The runtime is fully self-contained: a dedicated in-memory session/thread
|
|
11
11
|
* store backs the AgentManager, so sub-agent bookkeeping never touches the
|
|
12
12
|
* CLI's on-disk `/resume` conversation store.
|
|
13
13
|
*/
|
|
14
|
-
import { AgentManager, AgentRegistry, DefaultCapacityValidator, InMemorySessionStore, InMemoryTopicStore, LocalTaskScheduler, ReactiveAgent, RunCancelled, SessionSummaryMaterializer,
|
|
14
|
+
import { AgentManager, AgentRegistry, CompletionInbox, DefaultCapacityValidator, EXPLORE_AGENT_DESCRIPTION, EXPLORE_AGENT_ID, EXPLORE_AGENT_PROMPT, InMemorySessionStore, InMemoryTopicStore, LocalTaskScheduler, ReactiveAgent, RunCancelled, SessionSummaryMaterializer, TopicArchivedError, TopicManager, WorkspaceBackendRegistry, asTaskId, defineTool, filterReadOnlyTools, filterToolsNamed, generateSummaryId, isTerminalAgentTaskState, mcpJsonSchemaToZod, openTokenBudget, requireOpenProject, } from '@namzu/sdk';
|
|
15
15
|
import { NAMZU_WORKING_DOCTRINE } from '../../context/doctrine.js';
|
|
16
16
|
import { MAX_AGENT_ACTIVITY_LABEL_CODE_UNITS, MAX_AGENT_PHASE_ORDER, SubagentActivityMonitor, } from './activity.js';
|
|
17
17
|
import { CLI_INTERACTIVE_RUN_TIMEOUT_MS } from './policy.js';
|
|
18
18
|
export const GENERAL_PURPOSE_SUBAGENT = 'general-purpose';
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the delegations a parent makes most, and every one of them ran with
|
|
24
|
-
* `write`, `edit` and `bash` in the child's roster — so a lookup needed the
|
|
25
|
-
* same approvals as a change, and an operator in `accept-edits` mode was
|
|
26
|
-
* still asked about a child's shell command that only wanted to `cat`. This
|
|
27
|
-
* definition's registry is the parent's working set filtered to tools that
|
|
28
|
-
* declare themselves read-only, so a child that cannot change anything is
|
|
29
|
-
* never asked whether it may.
|
|
20
|
+
* The kernel's read-only delegate, with this application's doctrine under
|
|
21
|
+
* its prompt. Roster and prompt come from `@namzu/sdk`; what is added here is
|
|
22
|
+
* the same working rules every namzu child gets.
|
|
30
23
|
*/
|
|
31
|
-
export const EXPLORE_SUBAGENT =
|
|
32
|
-
const EXPLORE_PROMPT = [
|
|
33
|
-
'You are a read-only sub-agent dispatched by namzu to find things out: where something is defined, which files reference it, how a piece of code works, what a directory contains.',
|
|
34
|
-
'You cannot see the parent conversation — work only from the prompt you were given.',
|
|
35
|
-
'You have reading and searching tools only. You cannot edit, write or run commands, and you must not describe a change as made.',
|
|
36
|
-
'Search broadly first, then read what matters. Report file paths with line numbers, quote the relevant lines, and say plainly what you did not find.',
|
|
37
|
-
'End with a concise answer the parent can act on; do not ask the parent questions.',
|
|
38
|
-
'',
|
|
39
|
-
NAMZU_WORKING_DOCTRINE,
|
|
40
|
-
].join('\n');
|
|
24
|
+
export const EXPLORE_SUBAGENT = EXPLORE_AGENT_ID;
|
|
25
|
+
const EXPLORE_PROMPT = [EXPLORE_AGENT_PROMPT, '', NAMZU_WORKING_DOCTRINE].join('\n');
|
|
41
26
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* `defineTool` records — rather than by a name list here, so a new read-only
|
|
46
|
-
* builtin joins the explore roster without this file learning its name and
|
|
47
|
-
* a tool that stops being read-only leaves it the same way.
|
|
27
|
+
* A file-defined agent's roster: the parent's working set, kept to the
|
|
28
|
+
* file's `tools` allowlist when there is one, and to read-only tools when
|
|
29
|
+
* the file says `readOnly: true`. Intersection, never union.
|
|
48
30
|
*/
|
|
49
|
-
function
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
return filtered;
|
|
31
|
+
function fileAgentTools(definition, opts) {
|
|
32
|
+
return () => {
|
|
33
|
+
const source = definition.readOnly ? filterReadOnlyTools(opts.buildTools()) : opts.buildTools();
|
|
34
|
+
return definition.tools ? filterToolsNamed(source, definition.tools) : source;
|
|
35
|
+
};
|
|
56
36
|
}
|
|
57
37
|
const SUBAGENT_PROMPT = [
|
|
58
38
|
'You are a focused sub-agent dispatched by namzu to complete one self-contained task and report back.',
|
|
@@ -70,6 +50,39 @@ const SUBAGENT_PROMPT = [
|
|
|
70
50
|
// the parent does not is the same defect in the other direction.
|
|
71
51
|
NAMZU_WORKING_DOCTRINE,
|
|
72
52
|
].join('\n');
|
|
53
|
+
/** Recheck the invoking run before admitting work through a retained gateway. */
|
|
54
|
+
class ParentTaskScheduler extends LocalTaskScheduler {
|
|
55
|
+
validateParent;
|
|
56
|
+
constructor(manager, context, onEvent, validateParent) {
|
|
57
|
+
super(manager, context, onEvent);
|
|
58
|
+
this.validateParent = validateParent;
|
|
59
|
+
}
|
|
60
|
+
async createTask(options) {
|
|
61
|
+
await this.validateParent();
|
|
62
|
+
return super.createTask(options);
|
|
63
|
+
}
|
|
64
|
+
owns(taskId) {
|
|
65
|
+
return super.listTasks().some((task) => task.taskId === taskId);
|
|
66
|
+
}
|
|
67
|
+
getTask(taskId) {
|
|
68
|
+
return this.owns(taskId) ? super.getTask(taskId) : undefined;
|
|
69
|
+
}
|
|
70
|
+
cancelTask(taskId, cause) {
|
|
71
|
+
if (this.owns(taskId))
|
|
72
|
+
super.cancelTask(taskId, cause);
|
|
73
|
+
}
|
|
74
|
+
async waitForTask(taskId) {
|
|
75
|
+
if (!this.owns(taskId))
|
|
76
|
+
throw new Error(`Task ${taskId} does not belong to this parent run`);
|
|
77
|
+
return super.waitForTask(taskId);
|
|
78
|
+
}
|
|
79
|
+
async continueTask(taskId, message) {
|
|
80
|
+
if (!this.owns(taskId))
|
|
81
|
+
throw new Error(`Task ${taskId} does not belong to this parent run`);
|
|
82
|
+
await this.validateParent();
|
|
83
|
+
await super.continueTask(taskId, message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
73
86
|
/** A delegated run may never inherit the SDK's headless auto-approval fallback. */
|
|
74
87
|
const refuseUnownedChildReview = async () => ({
|
|
75
88
|
action: 'abort',
|
|
@@ -77,63 +90,216 @@ const refuseUnownedChildReview = async () => ({
|
|
|
77
90
|
});
|
|
78
91
|
/**
|
|
79
92
|
* Stand up the AgentManager + gateway + `Agent` tool. Returns the tool to
|
|
80
|
-
* register on the parent and the gateway to pass to `query({
|
|
93
|
+
* register on the parent and the gateway to pass to `query({ taskScheduler })`.
|
|
81
94
|
*/
|
|
82
95
|
export async function createSubagentRuntime(opts) {
|
|
83
|
-
const tenantId = asTenantId('tnt_namzu-cli');
|
|
84
|
-
const store = new InMemorySessionStore();
|
|
85
|
-
const topicStore = new InMemoryTopicStore();
|
|
86
|
-
const userActor = {
|
|
87
|
-
kind: 'user',
|
|
88
|
-
userId: asUserId('usr_namzu'),
|
|
89
|
-
tenantId,
|
|
90
|
-
};
|
|
91
|
-
const project = await store.createProject({ tenantId, name: 'namzu-cli' }, tenantId);
|
|
92
|
-
const thread = await topicStore.createTopic({ projectId: project.id, title: 'namzu-cli' }, tenantId);
|
|
93
|
-
// No workspace gate here, and that is a finding rather than an omission.
|
|
94
|
-
//
|
|
95
|
-
// A direct `createSession` bypasses `requireOpenProject`, which is why the
|
|
96
|
-
// CLI's persistent conversation store now calls it explicitly. This site is
|
|
97
|
-
// the other shape: the store is a fresh `InMemorySessionStore` built four
|
|
98
|
-
// lines up, the project was created two lines up, and neither outlives this
|
|
99
|
-
// runtime — so the id can never be one an owner has closed. A gate on a path
|
|
100
|
-
// that always creates its own project checks a condition that cannot be
|
|
101
|
-
// false, and a check that cannot fail teaches the next reader nothing except
|
|
102
|
-
// that gates here are decoration.
|
|
103
|
-
//
|
|
104
|
-
// The trigger to add one is the day this store is replaced by a persistent
|
|
105
|
-
// one, or the project id starts arriving from a caller.
|
|
106
|
-
const parentSession = await store.createSession({ topicId: thread.id, projectId: project.id, currentActor: userActor }, tenantId);
|
|
107
|
-
await store.updateSession({ ...parentSession, status: 'active' }, tenantId);
|
|
108
|
-
let summaryCounter = 0;
|
|
109
|
-
const materializer = new SessionSummaryMaterializer({
|
|
110
|
-
store,
|
|
111
|
-
generateSummaryId: () => asSummaryId(`sum_namzu_${++summaryCounter}`),
|
|
112
|
-
});
|
|
113
96
|
const registry = new AgentRegistry();
|
|
114
97
|
registry.register(buildDefinition(GENERAL_PURPOSE_SUBAGENT, 'A general-purpose sub-agent.', SUBAGENT_PROMPT, opts));
|
|
115
|
-
registry.register(buildDefinition(EXPLORE_SUBAGENT,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
98
|
+
registry.register(buildDefinition(EXPLORE_SUBAGENT, EXPLORE_AGENT_DESCRIPTION, EXPLORE_PROMPT, opts, () => filterReadOnlyTools(opts.buildTools())));
|
|
99
|
+
// Agents a project or user defined in a file, each a type of its own.
|
|
100
|
+
// The roster is the file's allowlist intersected with the parent's set —
|
|
101
|
+
// a file cannot grant a tool the parent does not have — narrowed further
|
|
102
|
+
// to read-only when the file says so. The prompt is the file's body over
|
|
103
|
+
// the same sub-agent base every child gets.
|
|
104
|
+
const fileAgents = new Map();
|
|
105
|
+
for (const definition of opts.definitions ?? []) {
|
|
106
|
+
fileAgents.set(definition.name, definition);
|
|
107
|
+
registry.register(buildDefinition(definition.name, definition.description, definition.prompt, opts, fileAgentTools(definition, opts), definition.model ?? opts.model));
|
|
108
|
+
}
|
|
109
|
+
const agentTypeIds = [GENERAL_PURPOSE_SUBAGENT, EXPLORE_SUBAGENT, ...fileAgents.keys()];
|
|
110
|
+
const fileAgentSummary = [...fileAgents.values()]
|
|
111
|
+
.map((definition) => `"${definition.name}" — ${definition.description}`)
|
|
112
|
+
.join('; ');
|
|
113
|
+
const parents = new Map();
|
|
114
|
+
const sessions = new Map();
|
|
115
|
+
let closed = false;
|
|
116
|
+
const sessionKey = ({ project, sessionId }) => JSON.stringify([project.tenantId, project.id, sessionId]);
|
|
117
|
+
const resolveParent = async (runId) => {
|
|
118
|
+
const parent = structuredClone(await opts.resolveParent(runId));
|
|
119
|
+
const { project, topic } = parent;
|
|
120
|
+
if (topic.projectId !== project.id || topic.tenantId !== project.tenantId) {
|
|
121
|
+
throw new Error('Delegation parent topic does not belong to its project and tenant');
|
|
122
|
+
}
|
|
123
|
+
await requireOpenProject({ getProject: async () => project }, project.id, project.tenantId, 'spawn');
|
|
124
|
+
if (topic.status === 'archived')
|
|
125
|
+
throw new TopicArchivedError({ topicId: topic.id, op: 'spawn' });
|
|
126
|
+
return parent;
|
|
127
|
+
};
|
|
128
|
+
const createSessionRuntime = async (parent) => {
|
|
129
|
+
const { project, topic, sessionId } = parent;
|
|
130
|
+
const tenantId = project.tenantId;
|
|
131
|
+
const store = new InMemorySessionStore([project]);
|
|
132
|
+
const topicStore = new InMemoryTopicStore([topic]);
|
|
133
|
+
const parentActor = { kind: 'agent', agentId: 'namzu', tenantId };
|
|
134
|
+
const session = await store.createSession({
|
|
135
|
+
id: sessionId,
|
|
136
|
+
topicId: topic.id,
|
|
137
|
+
projectId: project.id,
|
|
138
|
+
currentActor: parentActor,
|
|
139
|
+
}, tenantId);
|
|
140
|
+
await store.updateSession({ ...session, status: 'active' }, tenantId);
|
|
141
|
+
const manager = new AgentManager(registry, { childTimeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS }, {
|
|
142
|
+
sessionStore: store,
|
|
143
|
+
summaryMaterializer: new SessionSummaryMaterializer({
|
|
144
|
+
store,
|
|
145
|
+
generateSummaryId,
|
|
146
|
+
}),
|
|
147
|
+
workspaceRegistry: new WorkspaceBackendRegistry(),
|
|
148
|
+
capacity: new DefaultCapacityValidator(store),
|
|
149
|
+
topicManager: new TopicManager({ topicStore, sessionStore: store }),
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
manager,
|
|
153
|
+
store,
|
|
154
|
+
topicId: topic.id,
|
|
155
|
+
projectUpdatedAt: project.updatedAt.getTime(),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
const refreshLimits = async (shared, parent) => {
|
|
159
|
+
if (shared.topicId !== parent.topic.id)
|
|
160
|
+
throw new Error('Delegation parent changed its topic while runs are active');
|
|
161
|
+
// A slow metadata read must not overwrite a newer project snapshot.
|
|
162
|
+
const updatedAt = parent.project.updatedAt.getTime();
|
|
163
|
+
if (updatedAt < shared.projectUpdatedAt)
|
|
164
|
+
return;
|
|
165
|
+
shared.projectUpdatedAt = updatedAt;
|
|
166
|
+
await shared.store.updateProject(parent.project.id, parent.project.config, parent.project.tenantId);
|
|
167
|
+
};
|
|
168
|
+
const acquireSession = async (parent) => {
|
|
169
|
+
const key = sessionKey(parent);
|
|
170
|
+
let entry = sessions.get(key);
|
|
171
|
+
if (!entry) {
|
|
172
|
+
entry = { ready: createSessionRuntime(parent), owners: 0 };
|
|
173
|
+
sessions.set(key, entry);
|
|
174
|
+
}
|
|
175
|
+
entry.owners++;
|
|
176
|
+
const ownedEntry = entry;
|
|
177
|
+
let released = false;
|
|
178
|
+
const release = () => {
|
|
179
|
+
if (released)
|
|
180
|
+
return;
|
|
181
|
+
released = true;
|
|
182
|
+
ownedEntry.owners--;
|
|
183
|
+
if (ownedEntry.owners !== 0)
|
|
184
|
+
return;
|
|
185
|
+
if (sessions.get(key) === ownedEntry)
|
|
186
|
+
sessions.delete(key);
|
|
187
|
+
void ownedEntry.ready.then((shared) => shared.manager.dispose(), () => undefined);
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
const shared = await entry.ready;
|
|
191
|
+
await refreshLimits(shared, parent);
|
|
192
|
+
return { shared, release };
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
release();
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
const gatewayForRun = async (runId) => {
|
|
200
|
+
if (closed)
|
|
201
|
+
throw new Error('Sub-agent runtime is closed');
|
|
202
|
+
let pending = parents.get(runId);
|
|
203
|
+
if (!pending) {
|
|
204
|
+
pending = (async () => {
|
|
205
|
+
const parent = await resolveParent(runId);
|
|
206
|
+
const budget = await openTokenBudget({
|
|
207
|
+
scope: {
|
|
208
|
+
tenantId: parent.project.tenantId,
|
|
209
|
+
projectId: parent.project.id,
|
|
210
|
+
sessionId: parent.sessionId,
|
|
211
|
+
runId,
|
|
212
|
+
},
|
|
213
|
+
limit: opts.tokenBudget ?? 1_000_000,
|
|
214
|
+
pathBuilder: opts.pathBuilder,
|
|
215
|
+
workingDirectory: opts.cwd,
|
|
216
|
+
});
|
|
217
|
+
const lease = await acquireSession(parent);
|
|
218
|
+
const { manager } = lease.shared;
|
|
219
|
+
const parentAbortController = new AbortController();
|
|
220
|
+
let released = false;
|
|
221
|
+
const assertOwned = () => {
|
|
222
|
+
if (closed ||
|
|
223
|
+
released ||
|
|
224
|
+
parentAbortController.signal.aborted ||
|
|
225
|
+
parents.get(runId) !== pending) {
|
|
226
|
+
throw new Error(`Parent run ${runId} was released`);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
const taskContext = {
|
|
230
|
+
parentRunId: runId,
|
|
231
|
+
parentAgentId: 'namzu',
|
|
232
|
+
parentAbortController,
|
|
233
|
+
depth: 0,
|
|
234
|
+
budget,
|
|
235
|
+
tenantId: parent.project.tenantId,
|
|
236
|
+
topicId: parent.topic.id,
|
|
237
|
+
sessionId: parent.sessionId,
|
|
238
|
+
projectId: parent.project.id,
|
|
239
|
+
parentActor: {
|
|
240
|
+
kind: 'agent',
|
|
241
|
+
agentId: 'namzu',
|
|
242
|
+
tenantId: parent.project.tenantId,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
const gateway = new ParentTaskScheduler(manager, taskContext, opts.onEvent, async () => {
|
|
246
|
+
assertOwned();
|
|
247
|
+
const current = await resolveParent(runId);
|
|
248
|
+
assertOwned();
|
|
249
|
+
if (sessionKey(current) !== sessionKey(parent))
|
|
250
|
+
throw new Error('Delegation run changed its parent scope');
|
|
251
|
+
await refreshLimits(lease.shared, current);
|
|
252
|
+
assertOwned();
|
|
253
|
+
});
|
|
254
|
+
const completionInbox = new CompletionInbox();
|
|
255
|
+
completionInbox.attach(gateway);
|
|
256
|
+
const runtime = {
|
|
257
|
+
gateway,
|
|
258
|
+
completionInbox,
|
|
259
|
+
close() {
|
|
260
|
+
if (released)
|
|
261
|
+
return;
|
|
262
|
+
released = true;
|
|
263
|
+
parentAbortController.abort(new RunCancelled('parent'));
|
|
264
|
+
manager.cancelAll(runId, 'parent');
|
|
265
|
+
completionInbox.close();
|
|
266
|
+
lease.release();
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
if (closed) {
|
|
270
|
+
runtime.close();
|
|
271
|
+
throw new Error('Sub-agent runtime is closed');
|
|
272
|
+
}
|
|
273
|
+
return runtime;
|
|
274
|
+
})();
|
|
275
|
+
parents.set(runId, pending);
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
const runtime = await pending;
|
|
279
|
+
if (closed || parents.get(runId) !== pending)
|
|
280
|
+
throw new Error(`Parent run ${runId} was released`);
|
|
281
|
+
return runtime.gateway;
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
if (parents.get(runId) === pending)
|
|
285
|
+
parents.delete(runId);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
const releaseRun = async (runId) => {
|
|
290
|
+
const pending = parents.get(runId);
|
|
291
|
+
if (!pending)
|
|
292
|
+
return;
|
|
293
|
+
parents.delete(runId);
|
|
294
|
+
await pending.then((runtime) => runtime.close(), () => undefined);
|
|
295
|
+
};
|
|
296
|
+
const completionInboxForRun = async (runId) => {
|
|
297
|
+
await gatewayForRun(runId);
|
|
298
|
+
const pending = parents.get(runId);
|
|
299
|
+
if (!pending)
|
|
300
|
+
throw new Error(`Parent run ${runId} was released`);
|
|
301
|
+
return (await pending).completionInbox;
|
|
135
302
|
};
|
|
136
|
-
const gateway = new LocalTaskScheduler(manager, taskContext, opts.onEvent);
|
|
137
303
|
const activity = new SubagentActivityMonitor();
|
|
138
304
|
// Dynamic `Agent` tool: the model passes an optional `role` (the persona /
|
|
139
305
|
// system prompt) and we register + spawn a fresh specialist for it at call
|
|
@@ -141,19 +307,21 @@ export async function createSubagentRuntime(opts) {
|
|
|
141
307
|
let dynCounter = 0;
|
|
142
308
|
const agentTool = defineTool({
|
|
143
309
|
name: 'Agent',
|
|
144
|
-
description:
|
|
145
|
-
'
|
|
146
|
-
'files reference Y, how does Z work — it has reading and searching tools only and never asks for '
|
|
147
|
-
'
|
|
148
|
-
'Define a specialist inline with `role` — a system prompt describing who the sub-agent '
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
|
|
156
|
-
|
|
310
|
+
description: [
|
|
311
|
+
'Delegate a self-contained task to a sub-agent and wait for its result. If the operator sends a message while you wait, this call returns the running task ID so you can respond; the child keeps working and its result arrives later as a task notification.',
|
|
312
|
+
'Pick `subagent_type: "explore"` for anything that only needs to look — where is X defined, which files reference Y, how does Z work — it has reading and searching tools only and never asks for permission.',
|
|
313
|
+
'Use the default "general-purpose" when the task must change files or run commands.',
|
|
314
|
+
'Define a specialist inline with `role` — a system prompt describing who the sub-agent is and how to behave (e.g.',
|
|
315
|
+
'"You are a security auditor; flag vulnerabilities and rate severity"); with `subagent_type: "explore"` the role keeps the read-only roster.',
|
|
316
|
+
'Omit `role` for the plain sub-agent.',
|
|
317
|
+
'The sub-agent runs in its own context with its own tools and cannot see this conversation — put everything it needs in `prompt`.',
|
|
318
|
+
'Call this multiple times in one response to run specialists in parallel.',
|
|
319
|
+
'When coordinating several specialists, give them the same `workflow` label and an explicit `phase` plus `phase_order` so the operator can follow the work in the agent cockpit.',
|
|
320
|
+
'These fields are display annotations only; they do not create dependencies, barriers, or serial execution.',
|
|
321
|
+
fileAgentSummary.length > 0 ? `Project-defined types: ${fileAgentSummary}.` : '',
|
|
322
|
+
]
|
|
323
|
+
.filter((part) => part.length > 0)
|
|
324
|
+
.join(' '),
|
|
157
325
|
inputSchema: mcpJsonSchemaToZod({
|
|
158
326
|
type: 'object',
|
|
159
327
|
properties: {
|
|
@@ -167,8 +335,10 @@ export async function createSubagentRuntime(opts) {
|
|
|
167
335
|
},
|
|
168
336
|
subagent_type: {
|
|
169
337
|
type: 'string',
|
|
170
|
-
enum:
|
|
171
|
-
description:
|
|
338
|
+
enum: agentTypeIds,
|
|
339
|
+
description: `"explore" for read-only lookups (find, search, explain); "general-purpose" (default) when the task changes files or runs commands.${fileAgentSummary.length > 0
|
|
340
|
+
? ` This project also defines: ${fileAgentSummary}. Prefer a project-defined type when its description fits the task.`
|
|
341
|
+
: ''}`,
|
|
172
342
|
},
|
|
173
343
|
role: {
|
|
174
344
|
type: 'string',
|
|
@@ -202,15 +372,22 @@ export async function createSubagentRuntime(opts) {
|
|
|
202
372
|
async execute(input, context) {
|
|
203
373
|
const { description, prompt, subagent_type, role, workflow, phase, phase_order } = input;
|
|
204
374
|
const explore = subagent_type === EXPLORE_SUBAGENT;
|
|
205
|
-
|
|
375
|
+
const fileAgent = subagent_type !== undefined ? fileAgents.get(subagent_type) : undefined;
|
|
376
|
+
let agentId = fileAgent?.name ?? (explore ? EXPLORE_SUBAGENT : GENERAL_PURPOSE_SUBAGENT);
|
|
206
377
|
const persona = typeof role === 'string' ? role.trim() : '';
|
|
207
378
|
const dynamic = persona.length > 0;
|
|
208
379
|
if (dynamic) {
|
|
209
|
-
// A role on top of
|
|
210
|
-
// who the child is, the type says what it may touch,
|
|
211
|
-
// must not be a way to hand a read-only child a `write`
|
|
380
|
+
// A role on top of a type keeps that type's roster and model: the
|
|
381
|
+
// persona says who the child is, the type says what it may touch,
|
|
382
|
+
// and a role must not be a way to hand a read-only child a `write`
|
|
383
|
+
// tool. Over a file-defined agent the role is appended to the
|
|
384
|
+
// file's prompt rather than replacing it.
|
|
212
385
|
agentId = `dyn-${++dynCounter}`;
|
|
213
|
-
registry.register(buildDefinition(agentId, `Dynamic specialist: ${agentId}`,
|
|
386
|
+
registry.register(buildDefinition(agentId, `Dynamic specialist: ${agentId}`, fileAgent ? `${fileAgent.prompt}\n\n${persona}` : persona, opts, fileAgent
|
|
387
|
+
? fileAgentTools(fileAgent, opts)
|
|
388
|
+
: explore
|
|
389
|
+
? () => filterReadOnlyTools(opts.buildTools())
|
|
390
|
+
: opts.buildTools, fileAgent?.model ?? opts.model));
|
|
214
391
|
}
|
|
215
392
|
const tracker = activity.begin({
|
|
216
393
|
agentId,
|
|
@@ -234,10 +411,24 @@ export async function createSubagentRuntime(opts) {
|
|
|
234
411
|
...(Object.keys(context.env ?? {}).length > 0 ? { env: context.env } : {}),
|
|
235
412
|
resumeHandler,
|
|
236
413
|
};
|
|
414
|
+
let taskOwnsCleanup = false;
|
|
415
|
+
const cleanupDefinition = () => {
|
|
416
|
+
if (dynamic)
|
|
417
|
+
registry.unregister(agentId);
|
|
418
|
+
};
|
|
237
419
|
try {
|
|
238
|
-
const
|
|
239
|
-
|
|
420
|
+
const completionInbox = await completionInboxForRun(context.runId);
|
|
421
|
+
const outcome = await runBlockingAgentTask({
|
|
422
|
+
gateway: await gatewayForRun(context.runId),
|
|
240
423
|
signal: context.abortSignal,
|
|
424
|
+
waitForInbound: opts.resolveWaitForInbound?.(context.runId),
|
|
425
|
+
completionInbox,
|
|
426
|
+
onCreated: () => {
|
|
427
|
+
taskOwnsCleanup = true;
|
|
428
|
+
},
|
|
429
|
+
onSettled: (completed) => tracker.settle(completed),
|
|
430
|
+
onFailed: (error) => tracker.fail(error),
|
|
431
|
+
onFinished: cleanupDefinition,
|
|
241
432
|
create: {
|
|
242
433
|
agentId,
|
|
243
434
|
prompt,
|
|
@@ -252,59 +443,105 @@ export async function createSubagentRuntime(opts) {
|
|
|
252
443
|
onEvent: tracker.onEvent,
|
|
253
444
|
},
|
|
254
445
|
});
|
|
255
|
-
|
|
256
|
-
const runStatus = completed.result?.status;
|
|
257
|
-
const succeeded = completed.state === 'completed' && (runStatus === undefined || runStatus === 'completed');
|
|
258
|
-
const resultText = typeof completed.result?.result === 'string'
|
|
259
|
-
? completed.result.result
|
|
260
|
-
: completed.result?.result !== undefined
|
|
261
|
-
? JSON.stringify(completed.result.result)
|
|
262
|
-
: '';
|
|
263
|
-
if (!succeeded) {
|
|
446
|
+
if (outcome.kind === 'yielded') {
|
|
264
447
|
return {
|
|
265
|
-
success:
|
|
266
|
-
output:
|
|
267
|
-
|
|
448
|
+
success: true,
|
|
449
|
+
output: `Sub-agent ${agentId} is still running as task ${outcome.handle.taskId}; it has not completed. Waiting was released because the operator sent a message. Respond to that message while this task continues. Its actual result will arrive as a task notification; do not launch the same work again.`,
|
|
450
|
+
data: {
|
|
451
|
+
task_id: outcome.handle.taskId,
|
|
452
|
+
state: outcome.handle.state,
|
|
453
|
+
wait_released: 'operator_input',
|
|
454
|
+
},
|
|
268
455
|
};
|
|
269
456
|
}
|
|
270
|
-
return
|
|
271
|
-
success: true,
|
|
272
|
-
output: resultText || '(sub-agent returned no text)',
|
|
273
|
-
};
|
|
457
|
+
return completedAgentResult(outcome.handle);
|
|
274
458
|
}
|
|
275
459
|
catch (error) {
|
|
276
460
|
tracker.fail(error);
|
|
277
461
|
throw error;
|
|
278
462
|
}
|
|
279
463
|
finally {
|
|
280
|
-
// A
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
registry.unregister(agentId);
|
|
464
|
+
// A yielded task retains its definition until its actual completion.
|
|
465
|
+
// Before creation succeeds, this invocation owns that cleanup.
|
|
466
|
+
if (!taskOwnsCleanup)
|
|
467
|
+
cleanupDefinition();
|
|
285
468
|
}
|
|
286
469
|
},
|
|
287
470
|
});
|
|
471
|
+
const waitForTaskTool = defineTool({
|
|
472
|
+
name: 'wait_for_task',
|
|
473
|
+
description: 'Wait for a task this run already launched, or retrieve its complete result after a task notification. This does not start new work. Operator input releases the wait while the task continues.',
|
|
474
|
+
inputSchema: mcpJsonSchemaToZod({
|
|
475
|
+
type: 'object',
|
|
476
|
+
properties: {
|
|
477
|
+
task_id: {
|
|
478
|
+
type: 'string',
|
|
479
|
+
description: 'The task ID returned by Agent or a task notification.',
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
required: ['task_id'],
|
|
483
|
+
}),
|
|
484
|
+
category: 'custom',
|
|
485
|
+
permissions: [],
|
|
486
|
+
readOnly: true,
|
|
487
|
+
destructive: false,
|
|
488
|
+
concurrencySafe: true,
|
|
489
|
+
timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
490
|
+
async execute(input, context) {
|
|
491
|
+
const gateway = await gatewayForRun(context.runId);
|
|
492
|
+
let taskId;
|
|
493
|
+
try {
|
|
494
|
+
taskId = asTaskId(input.task_id);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
output: '',
|
|
500
|
+
error: 'task_id must be a task UUID returned by Agent or a task notification.',
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const task = gateway.getTask(taskId);
|
|
504
|
+
if (!task)
|
|
505
|
+
return {
|
|
506
|
+
success: false,
|
|
507
|
+
output: '',
|
|
508
|
+
error: `Task ${taskId} does not belong to this parent run.`,
|
|
509
|
+
};
|
|
510
|
+
const outcome = await runBlockingAgentTask({
|
|
511
|
+
gateway,
|
|
512
|
+
task,
|
|
513
|
+
signal: context.abortSignal,
|
|
514
|
+
completionInbox: await completionInboxForRun(context.runId),
|
|
515
|
+
waitForInbound: opts.resolveWaitForInbound?.(context.runId),
|
|
516
|
+
onCreated: () => { },
|
|
517
|
+
onSettled: () => { },
|
|
518
|
+
onFailed: () => { },
|
|
519
|
+
onFinished: () => { },
|
|
520
|
+
});
|
|
521
|
+
if (outcome.kind === 'completed')
|
|
522
|
+
return completedAgentResult(outcome.handle);
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
output: `Task ${taskId} is still running; it has not completed. Waiting was released for an operator message. Its result will arrive as a task notification.`,
|
|
526
|
+
data: { task_id: taskId, state: outcome.handle.state, wait_released: 'operator_input' },
|
|
527
|
+
};
|
|
528
|
+
},
|
|
529
|
+
});
|
|
288
530
|
let closePromise;
|
|
289
531
|
const close = () => {
|
|
290
532
|
if (closePromise)
|
|
291
533
|
return closePromise;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// Abort it before disposing bookkeeping so a live child keeps the
|
|
295
|
-
// structured reason even if its blocking tool wait already detached.
|
|
296
|
-
if (!taskContext.parentAbortController.signal.aborted) {
|
|
297
|
-
taskContext.parentAbortController.abort(new RunCancelled('parent'));
|
|
298
|
-
}
|
|
299
|
-
manager.dispose();
|
|
300
|
-
activity.close();
|
|
301
|
-
});
|
|
534
|
+
closed = true;
|
|
535
|
+
closePromise = Promise.all([...parents.keys()].map(releaseRun)).then(() => activity.close());
|
|
302
536
|
return closePromise;
|
|
303
537
|
};
|
|
304
538
|
return {
|
|
305
|
-
|
|
539
|
+
gatewayForRun,
|
|
540
|
+
completionInboxForRun,
|
|
541
|
+
releaseRun,
|
|
306
542
|
agentTool,
|
|
307
|
-
|
|
543
|
+
waitForTaskTool,
|
|
544
|
+
allowedAgentIds: agentTypeIds,
|
|
308
545
|
activity,
|
|
309
546
|
close,
|
|
310
547
|
};
|
|
@@ -316,6 +553,7 @@ async function runBlockingAgentTask(input) {
|
|
|
316
553
|
let handle;
|
|
317
554
|
let cancellationRequested = false;
|
|
318
555
|
let taskCancellationAttempted = false;
|
|
556
|
+
const wakeController = new AbortController();
|
|
319
557
|
let rejectAbort = () => { };
|
|
320
558
|
const aborted = new Promise((_resolve, reject) => {
|
|
321
559
|
rejectAbort = reject;
|
|
@@ -341,7 +579,7 @@ async function runBlockingAgentTask(input) {
|
|
|
341
579
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
342
580
|
if (signal.aborted)
|
|
343
581
|
onAbort();
|
|
344
|
-
const creation = gateway.createTask(input.create);
|
|
582
|
+
const creation = input.create === undefined ? Promise.resolve(input.task) : gateway.createTask(input.create);
|
|
345
583
|
creation.catch(() => { });
|
|
346
584
|
try {
|
|
347
585
|
handle = await Promise.race([creation, aborted]);
|
|
@@ -349,8 +587,30 @@ async function runBlockingAgentTask(input) {
|
|
|
349
587
|
cancel(handle);
|
|
350
588
|
signal.throwIfAborted();
|
|
351
589
|
}
|
|
352
|
-
|
|
353
|
-
|
|
590
|
+
input.onCreated();
|
|
591
|
+
input.completionInbox.launched(handle.taskId);
|
|
592
|
+
const completion = gateway.waitForTask(handle.taskId).then((completed) => {
|
|
593
|
+
input.onSettled(completed);
|
|
594
|
+
input.onFinished();
|
|
595
|
+
return { kind: 'completed', handle: completed };
|
|
596
|
+
}, (error) => {
|
|
597
|
+
input.onFailed(error);
|
|
598
|
+
input.onFinished();
|
|
599
|
+
throw error;
|
|
600
|
+
});
|
|
601
|
+
const inbound = input
|
|
602
|
+
.waitForInbound?.(wakeController.signal)
|
|
603
|
+
.then(() => ({ kind: 'inbound' }));
|
|
604
|
+
const outcome = await Promise.race(inbound ? [completion, aborted, inbound] : [completion, aborted]);
|
|
605
|
+
const current = gateway.getTask(handle.taskId);
|
|
606
|
+
if (outcome.kind === 'completed' || (current && isTerminalAgentTaskState(current.state))) {
|
|
607
|
+
input.completionInbox.claim(handle.taskId);
|
|
608
|
+
return outcome.kind === 'completed' ? outcome : await completion;
|
|
609
|
+
}
|
|
610
|
+
// Only the wait has ended. Parent runtime ownership, token reservations and
|
|
611
|
+
// the completion observer all outlive this tool invocation's local signal.
|
|
612
|
+
input.completionInbox.expect(handle.taskId);
|
|
613
|
+
return { kind: 'yielded', handle };
|
|
354
614
|
}
|
|
355
615
|
catch (error) {
|
|
356
616
|
if (handle)
|
|
@@ -362,8 +622,46 @@ async function runBlockingAgentTask(input) {
|
|
|
362
622
|
void creation.then(cancel, () => { });
|
|
363
623
|
}
|
|
364
624
|
signal.removeEventListener('abort', onAbort);
|
|
625
|
+
wakeController.abort();
|
|
365
626
|
}
|
|
366
627
|
}
|
|
628
|
+
/** Lifecycle completion is not proof the requested task finished successfully. */
|
|
629
|
+
function completedAgentResult(completed) {
|
|
630
|
+
const run = completed.result;
|
|
631
|
+
const succeeded = completed.state === 'completed' && (run?.status === undefined || run.status === 'completed');
|
|
632
|
+
const value = run?.structuredOutput ?? run?.result;
|
|
633
|
+
let resultText = typeof value === 'string' ? value : value === undefined ? '' : JSON.stringify(value);
|
|
634
|
+
if (!resultText && run?.stopReason && run.stopReason !== 'end_turn') {
|
|
635
|
+
// A hard limit can stop between tool rounds without setting Run.result.
|
|
636
|
+
// Keep the child's last visible statement, never its reasoning or tool data.
|
|
637
|
+
const partial = [...run.messages]
|
|
638
|
+
.reverse()
|
|
639
|
+
.find((message) => message.role === 'assistant' &&
|
|
640
|
+
typeof message.content === 'string' &&
|
|
641
|
+
message.content.trim().length > 0)?.content;
|
|
642
|
+
if (typeof partial === 'string')
|
|
643
|
+
resultText = partial;
|
|
644
|
+
}
|
|
645
|
+
const stopNote = run?.stopReason && run.stopReason !== 'end_turn'
|
|
646
|
+
? `Sub-agent run ended with stop reason "${run.stopReason}". Output may be partial; this does not establish task completion.\n\n`
|
|
647
|
+
: '';
|
|
648
|
+
const output = stopNote + (resultText || '(sub-agent returned no text)');
|
|
649
|
+
return {
|
|
650
|
+
success: succeeded,
|
|
651
|
+
output,
|
|
652
|
+
...(!succeeded
|
|
653
|
+
? {
|
|
654
|
+
error: `Sub-agent ${completed.agentId} ${completed.state}: ${run?.lastError ?? ''}\n${output}`,
|
|
655
|
+
}
|
|
656
|
+
: {}),
|
|
657
|
+
data: {
|
|
658
|
+
task_id: completed.taskId,
|
|
659
|
+
state: completed.state,
|
|
660
|
+
...(run?.status ? { status: run.status } : {}),
|
|
661
|
+
...(run?.stopReason ? { stop_reason: run.stopReason } : {}),
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
}
|
|
367
665
|
/**
|
|
368
666
|
* Build an agent definition with the given id + persona (system prompt). Used
|
|
369
667
|
* for the static `general-purpose` agent and for each dynamically-defined
|
|
@@ -371,7 +669,9 @@ async function runBlockingAgentTask(input) {
|
|
|
371
669
|
*/
|
|
372
670
|
function buildDefinition(id, description, systemPrompt, opts,
|
|
373
671
|
/** This definition's own roster; absent means the parent's working set. */
|
|
374
|
-
tools = opts.buildTools
|
|
672
|
+
tools = opts.buildTools,
|
|
673
|
+
/** This definition's model; absent means the session's. */
|
|
674
|
+
model = opts.model) {
|
|
375
675
|
const agent = new ReactiveAgent({
|
|
376
676
|
id,
|
|
377
677
|
name: id,
|
|
@@ -391,7 +691,7 @@ tools = opts.buildTools) {
|
|
|
391
691
|
category: 'general',
|
|
392
692
|
description,
|
|
393
693
|
tools: [],
|
|
394
|
-
defaults: { model
|
|
694
|
+
defaults: { model, tokenBudget: 200_000 },
|
|
395
695
|
},
|
|
396
696
|
// ReactiveAgent is Agent<ReactiveAgentConfig,…>; the registry stores the
|
|
397
697
|
// erased Agent<BaseAgentConfig,…>. configBuilder supplies the richer config.
|
|
@@ -403,7 +703,7 @@ tools = opts.buildTools) {
|
|
|
403
703
|
// is worse than one that was never told.
|
|
404
704
|
const environment = opts.readEnvironment ? await opts.readEnvironment() : null;
|
|
405
705
|
return {
|
|
406
|
-
model: options.model ??
|
|
706
|
+
model: options.model ?? model,
|
|
407
707
|
tokenBudget: options.tokenBudget ?? 200_000,
|
|
408
708
|
timeoutMs: options.timeoutMs ?? CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
409
709
|
maxIterations: 40,
|