@namzu/cli 12.1.0 → 14.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/README.md +35 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +119 -60
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts +24 -8
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +221 -80
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +45 -9
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +79 -21
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/prior-messages.d.ts +11 -0
- package/dist/commands/prior-messages.d.ts.map +1 -0
- package/dist/commands/prior-messages.js +280 -0
- package/dist/commands/prior-messages.js.map +1 -0
- package/dist/commands/resume.d.ts.map +1 -1
- package/dist/commands/resume.js +21 -19
- package/dist/commands/resume.js.map +1 -1
- package/dist/commands/run-stream.d.ts +5 -3
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +66 -62
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +50 -37
- package/dist/commands/run.js.map +1 -1
- package/dist/config/debug.d.ts +55 -0
- package/dist/config/debug.d.ts.map +1 -0
- package/dist/config/debug.js +126 -0
- package/dist/config/debug.js.map +1 -0
- package/dist/config/load.d.ts +88 -4
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +386 -56
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +87 -5
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +5 -5
- package/dist/config/schema.js.map +1 -1
- package/dist/config/trusted-project-context.d.ts +11 -0
- package/dist/config/trusted-project-context.d.ts.map +1 -0
- package/dist/config/trusted-project-context.js +28 -0
- package/dist/config/trusted-project-context.js.map +1 -0
- package/dist/context/project-path.d.ts +3 -0
- package/dist/context/project-path.d.ts.map +1 -0
- package/dist/context/project-path.js +16 -0
- package/dist/context/project-path.js.map +1 -0
- package/dist/context/project-tracker.d.ts +30 -0
- package/dist/context/project-tracker.d.ts.map +1 -0
- package/dist/context/project-tracker.js +217 -0
- package/dist/context/project-tracker.js.map +1 -0
- package/dist/context/sandbox.d.ts +29 -9
- package/dist/context/sandbox.d.ts.map +1 -1
- package/dist/context/sandbox.js +8 -0
- package/dist/context/sandbox.js.map +1 -1
- package/dist/doctor/checks/credentials.d.ts.map +1 -1
- package/dist/doctor/checks/credentials.js +7 -1
- package/dist/doctor/checks/credentials.js.map +1 -1
- package/dist/exit-codes.d.ts +5 -4
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +5 -4
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/clipboard/text.d.ts +43 -0
- package/dist/integrations/clipboard/text.d.ts.map +1 -0
- package/dist/integrations/clipboard/text.js +43 -0
- package/dist/integrations/clipboard/text.js.map +1 -0
- package/dist/integrations/mcp/servers.d.ts +9 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +11 -4
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/notifications/terminal.d.ts +32 -0
- package/dist/integrations/notifications/terminal.d.ts.map +1 -0
- package/dist/integrations/notifications/terminal.js +46 -0
- package/dist/integrations/notifications/terminal.js.map +1 -0
- package/dist/integrations/plugins/runtime.d.ts +17 -0
- package/dist/integrations/plugins/runtime.d.ts.map +1 -0
- package/dist/integrations/plugins/runtime.js +100 -0
- package/dist/integrations/plugins/runtime.js.map +1 -0
- package/dist/integrations/providers/codex-device-login.d.ts +27 -0
- package/dist/integrations/providers/codex-device-login.d.ts.map +1 -0
- package/dist/integrations/providers/codex-device-login.js +195 -0
- package/dist/integrations/providers/codex-device-login.js.map +1 -0
- package/dist/integrations/providers/codex-oauth.d.ts +10 -0
- package/dist/integrations/providers/codex-oauth.d.ts.map +1 -0
- package/dist/integrations/providers/codex-oauth.js +95 -0
- package/dist/integrations/providers/codex-oauth.js.map +1 -0
- package/dist/integrations/providers/credential-store.d.ts +40 -4
- package/dist/integrations/providers/credential-store.d.ts.map +1 -1
- package/dist/integrations/providers/credential-store.js +236 -18
- package/dist/integrations/providers/credential-store.js.map +1 -1
- package/dist/integrations/providers/discover.d.ts +25 -13
- package/dist/integrations/providers/discover.d.ts.map +1 -1
- package/dist/integrations/providers/discover.js +98 -44
- package/dist/integrations/providers/discover.js.map +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts +36 -0
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -0
- package/dist/integrations/providers/harness-credentials.js +146 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -0
- package/dist/integrations/providers/identity.d.ts +8 -15
- package/dist/integrations/providers/identity.d.ts.map +1 -1
- package/dist/integrations/providers/identity.js +8 -15
- package/dist/integrations/providers/identity.js.map +1 -1
- package/dist/integrations/providers/index.d.ts +7 -4
- package/dist/integrations/providers/index.d.ts.map +1 -1
- package/dist/integrations/providers/index.js +6 -3
- package/dist/integrations/providers/index.js.map +1 -1
- package/dist/integrations/providers/keychain.d.ts +0 -13
- package/dist/integrations/providers/keychain.d.ts.map +1 -1
- package/dist/integrations/providers/keychain.js +0 -76
- package/dist/integrations/providers/keychain.js.map +1 -1
- package/dist/integrations/providers/oauth.d.ts +46 -17
- package/dist/integrations/providers/oauth.d.ts.map +1 -1
- package/dist/integrations/providers/oauth.js +175 -45
- package/dist/integrations/providers/oauth.js.map +1 -1
- package/dist/integrations/providers/register.d.ts.map +1 -1
- package/dist/integrations/providers/register.js +9 -1
- package/dist/integrations/providers/register.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +8 -1
- package/dist/integrations/providers/registry.d.ts.map +1 -1
- package/dist/integrations/providers/registry.js +29 -0
- package/dist/integrations/providers/registry.js.map +1 -1
- package/dist/integrations/providers/subscription-login.d.ts +2 -0
- package/dist/integrations/providers/subscription-login.d.ts.map +1 -1
- package/dist/integrations/providers/subscription-login.js +117 -14
- package/dist/integrations/providers/subscription-login.js.map +1 -1
- package/dist/integrations/sessions/store.d.ts +111 -1
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +400 -7
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts +25 -0
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -0
- package/dist/integrations/sessions/transcript-export.js +441 -0
- package/dist/integrations/sessions/transcript-export.js.map +1 -0
- package/dist/integrations/sessions/turn-evidence.d.ts +138 -0
- package/dist/integrations/sessions/turn-evidence.d.ts.map +1 -0
- package/dist/integrations/sessions/turn-evidence.js +506 -0
- package/dist/integrations/sessions/turn-evidence.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +7 -12
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +19 -10
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/integrations/trust/store.d.ts.map +1 -1
- package/dist/integrations/trust/store.js +27 -13
- package/dist/integrations/trust/store.js.map +1 -1
- package/dist/permissions/canonical-project.d.ts +9 -0
- package/dist/permissions/canonical-project.d.ts.map +1 -0
- package/dist/permissions/canonical-project.js +13 -0
- package/dist/permissions/canonical-project.js.map +1 -0
- package/dist/permissions/checks.d.ts +65 -0
- package/dist/permissions/checks.d.ts.map +1 -0
- package/dist/permissions/checks.js +122 -0
- package/dist/permissions/checks.js.map +1 -0
- package/dist/permissions/headless-trust.d.ts +9 -5
- package/dist/permissions/headless-trust.d.ts.map +1 -1
- package/dist/permissions/headless-trust.js +18 -4
- package/dist/permissions/headless-trust.js.map +1 -1
- package/dist/permissions/rules.d.ts +16 -7
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +78 -13
- package/dist/permissions/rules.js.map +1 -1
- package/dist/tui/App.d.ts +5 -2
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +1621 -196
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/Composer.d.ts +14 -3
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +53 -15
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/EditPromptPicker.d.ts +8 -0
- package/dist/tui/EditPromptPicker.d.ts.map +1 -0
- package/dist/tui/EditPromptPicker.js +18 -0
- package/dist/tui/EditPromptPicker.js.map +1 -0
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +2 -1
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/Markdown.d.ts.map +1 -1
- package/dist/tui/Markdown.js +2 -1
- package/dist/tui/Markdown.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +7 -1
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +8 -6
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +145 -18
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/ResumePicker.d.ts.map +1 -1
- package/dist/tui/ResumePicker.js +1 -1
- package/dist/tui/ResumePicker.js.map +1 -1
- package/dist/tui/Transcript.d.ts +4 -2
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +31 -5
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +123 -19
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +757 -144
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/compact-transcript.d.ts +24 -0
- package/dist/tui/compact-transcript.d.ts.map +1 -0
- package/dist/tui/compact-transcript.js +46 -0
- package/dist/tui/compact-transcript.js.map +1 -0
- package/dist/tui/conversation-history.d.ts +33 -0
- package/dist/tui/conversation-history.d.ts.map +1 -0
- package/dist/tui/conversation-history.js +42 -0
- package/dist/tui/conversation-history.js.map +1 -0
- package/dist/tui/edit-prompts.d.ts +23 -0
- package/dist/tui/edit-prompts.d.ts.map +1 -0
- package/dist/tui/edit-prompts.js +35 -0
- package/dist/tui/edit-prompts.js.map +1 -0
- package/dist/tui/live-window.d.ts +3 -1
- package/dist/tui/live-window.d.ts.map +1 -1
- package/dist/tui/live-window.js +20 -8
- package/dist/tui/live-window.js.map +1 -1
- package/dist/tui/login-prompt.d.ts +5 -0
- package/dist/tui/login-prompt.d.ts.map +1 -1
- package/dist/tui/login-prompt.js +14 -0
- package/dist/tui/login-prompt.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +178 -9
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +340 -16
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/terminal-display.d.ts +12 -0
- package/dist/tui/terminal-display.d.ts.map +1 -0
- package/dist/tui/terminal-display.js +65 -0
- package/dist/tui/terminal-display.js.map +1 -0
- package/dist/tui/types.d.ts +14 -2
- package/dist/tui/types.d.ts.map +1 -1
- package/dist/tui/workspace-diff.d.ts +40 -0
- package/dist/tui/workspace-diff.d.ts.map +1 -0
- package/dist/tui/workspace-diff.js +101 -0
- package/dist/tui/workspace-diff.js.map +1 -0
- package/package.json +8 -8
package/dist/tui/agent.js
CHANGED
|
@@ -20,17 +20,214 @@
|
|
|
20
20
|
* `emptySession()` whose `send()` yields a single error event so the UI
|
|
21
21
|
* renders an actionable hint rather than crashing.
|
|
22
22
|
*/
|
|
23
|
-
import { BOOT_EVENT_NAMES, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, ProviderRegistry, SearchToolsTool, ToolRegistry, asProjectId, asRunId, asSessionId, asTenantId, asTopicId, buildMemoryTools, createMemoryPromoter, createToolPresenter, genericLabel, getBuiltinTools, isTrustedReadOnly, query, resumeRun, } from '@namzu/sdk';
|
|
23
|
+
import { BOOT_EVENT_NAMES, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, asProjectId, asRunId, asSessionId, asTenantId, asTopicId, buildMemoryTools, buildSessionGoalTools, compactNow, createMemoryPromoter, createToolPresenter, genericLabel, getBuiltinTools, isTrustedReadOnly, query, resumeRun, withProviderFallback, } from '@namzu/sdk';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
import { probeCapabilities } from '../context/capabilities.js';
|
|
26
26
|
import { composeEnvironmentPrompt, readEnvironmentFacts } from '../context/environment.js';
|
|
27
|
-
import {
|
|
27
|
+
import { ProjectInstructionTracker } from '../context/project-tracker.js';
|
|
28
28
|
import { resolveSandbox, sandboxResolvedSeverity, } from '../context/sandbox.js';
|
|
29
29
|
import { connectMcpServers, } from '../integrations/mcp/servers.js';
|
|
30
|
-
import {
|
|
30
|
+
import { createCliPluginRuntime } from '../integrations/plugins/runtime.js';
|
|
31
|
+
import { CredentialRefreshRejectedError, CredentialWithdrawnError, PROVIDER_REGISTRY, chainCapabilityDisagreements, chainPositionName, describeAcceptedMismatch, describeCapabilityRefusal, discoverProviders, ensureFreshAnthropicToken, ensureFreshStoredCodexCredential, ensureRegistered, findDetected, isAnthropicOAuthToken, isRegistered, missingCredentialMessage, primaryProvider, readCodexCredentialFile, readPreferences, readSubscriptionCredential, resolveChainCapabilities, sameOAuthCredential, unresolvedMembers, unsupportedProviderMessage, } from '../integrations/providers/index.js';
|
|
31
32
|
import { createSubagentRuntime } from '../integrations/subagents/runtime.js';
|
|
32
33
|
import { cliLogger } from '../logging.js';
|
|
33
34
|
import { composeMemoryPrompt, readMemory } from '../memory/store.js';
|
|
35
|
+
import { projectRunConversation } from './conversation-history.js';
|
|
36
|
+
/**
|
|
37
|
+
* Let one caller stop waiting without cutting a shared queue in the middle.
|
|
38
|
+
*
|
|
39
|
+
* The queued operation remains chained and owns its own signal. This observer
|
|
40
|
+
* only settles the caller promptly; releasing a queue slot here would allow a
|
|
41
|
+
* later owner to overlap the operation still ahead of it.
|
|
42
|
+
*/
|
|
43
|
+
async function observeWithSignal(operation, signal) {
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
let rejectAbort = () => { };
|
|
46
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
47
|
+
rejectAbort = reject;
|
|
48
|
+
});
|
|
49
|
+
const onAbort = () => rejectAbort(signal.reason);
|
|
50
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
51
|
+
if (signal.aborted)
|
|
52
|
+
onAbort();
|
|
53
|
+
try {
|
|
54
|
+
return await Promise.race([operation, aborted]);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
signal.removeEventListener('abort', onAbort);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Own every asynchronous operation that can still touch a session resource.
|
|
62
|
+
*
|
|
63
|
+
* A caller-owned signal remains caller-owned: closing the session aborts the
|
|
64
|
+
* fused operation signal, never the controller the caller passed. Streams are
|
|
65
|
+
* registered when handed out rather than when first pulled, so a consumer that
|
|
66
|
+
* abandons one between yields cannot leave the session's provider/tool owners
|
|
67
|
+
* alive past `close()`.
|
|
68
|
+
*/
|
|
69
|
+
export class SessionOperationOwner {
|
|
70
|
+
cleanup;
|
|
71
|
+
lifetime = new AbortController();
|
|
72
|
+
active = new Set();
|
|
73
|
+
streamClosers = new Set();
|
|
74
|
+
closeReason = new DOMException('Agent session closed.', 'AbortError');
|
|
75
|
+
closed = false;
|
|
76
|
+
closePromise;
|
|
77
|
+
constructor(cleanup) {
|
|
78
|
+
this.cleanup = cleanup;
|
|
79
|
+
}
|
|
80
|
+
promise(callerSignal, start) {
|
|
81
|
+
let signal;
|
|
82
|
+
try {
|
|
83
|
+
signal = this.operationSignal(callerSignal);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return Promise.reject(error);
|
|
87
|
+
}
|
|
88
|
+
const operation = Promise.resolve().then(() => {
|
|
89
|
+
signal.throwIfAborted();
|
|
90
|
+
return start(signal);
|
|
91
|
+
});
|
|
92
|
+
this.track(operation);
|
|
93
|
+
return operation;
|
|
94
|
+
}
|
|
95
|
+
stream(callerSignal, start) {
|
|
96
|
+
let signal;
|
|
97
|
+
try {
|
|
98
|
+
signal = this.operationSignal(callerSignal);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
return {
|
|
102
|
+
[Symbol.asyncIterator]() {
|
|
103
|
+
return {
|
|
104
|
+
next: () => Promise.reject(error),
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const source = (async function* () {
|
|
110
|
+
signal.throwIfAborted();
|
|
111
|
+
yield* start(signal);
|
|
112
|
+
})()[Symbol.asyncIterator]();
|
|
113
|
+
let settled = false;
|
|
114
|
+
let settle;
|
|
115
|
+
const settlement = new Promise((resolve) => {
|
|
116
|
+
settle = resolve;
|
|
117
|
+
});
|
|
118
|
+
let operationTail = Promise.resolve();
|
|
119
|
+
let returnStarted = false;
|
|
120
|
+
let closeStreamPromise;
|
|
121
|
+
const finish = () => {
|
|
122
|
+
if (settled)
|
|
123
|
+
return;
|
|
124
|
+
settled = true;
|
|
125
|
+
this.streamClosers.delete(closeStream);
|
|
126
|
+
settle();
|
|
127
|
+
};
|
|
128
|
+
const enqueue = (operation) => {
|
|
129
|
+
const result = operationTail.then(operation);
|
|
130
|
+
operationTail = result.then(() => { }, () => { });
|
|
131
|
+
return result;
|
|
132
|
+
};
|
|
133
|
+
const observe = async (operation) => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await enqueue(operation);
|
|
136
|
+
if (result.done)
|
|
137
|
+
finish();
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
finish();
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const requestReturn = (value) => {
|
|
146
|
+
return observe(async () => {
|
|
147
|
+
returnStarted = true;
|
|
148
|
+
return source.return
|
|
149
|
+
? await source.return(value)
|
|
150
|
+
: { done: true, value };
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
const closeStream = () => {
|
|
154
|
+
if (closeStreamPromise)
|
|
155
|
+
return closeStreamPromise;
|
|
156
|
+
closeStreamPromise = (async () => {
|
|
157
|
+
try {
|
|
158
|
+
await enqueue(async () => {
|
|
159
|
+
if (settled)
|
|
160
|
+
return;
|
|
161
|
+
let result;
|
|
162
|
+
if (returnStarted) {
|
|
163
|
+
result = await source.next();
|
|
164
|
+
}
|
|
165
|
+
else if (source.return) {
|
|
166
|
+
returnStarted = true;
|
|
167
|
+
result = await source.return(undefined);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
while (!result.done)
|
|
173
|
+
result = await source.next();
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
finish();
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
180
|
+
return closeStreamPromise;
|
|
181
|
+
};
|
|
182
|
+
const managed = {
|
|
183
|
+
next: (value) => observe(() => source.next(value)),
|
|
184
|
+
return: requestReturn,
|
|
185
|
+
throw: (error) => observe(async () => {
|
|
186
|
+
if (source.throw)
|
|
187
|
+
return await source.throw(error);
|
|
188
|
+
throw error;
|
|
189
|
+
}),
|
|
190
|
+
[Symbol.asyncIterator]() {
|
|
191
|
+
return this;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
this.streamClosers.add(closeStream);
|
|
195
|
+
this.track(settlement);
|
|
196
|
+
return managed;
|
|
197
|
+
}
|
|
198
|
+
close() {
|
|
199
|
+
if (this.closePromise)
|
|
200
|
+
return this.closePromise;
|
|
201
|
+
this.closed = true;
|
|
202
|
+
this.lifetime.abort(this.closeReason);
|
|
203
|
+
this.closePromise = this.finishClose();
|
|
204
|
+
return this.closePromise;
|
|
205
|
+
}
|
|
206
|
+
operationSignal(callerSignal) {
|
|
207
|
+
if (this.closed)
|
|
208
|
+
throw this.closeReason;
|
|
209
|
+
return callerSignal
|
|
210
|
+
? AbortSignal.any([callerSignal, this.lifetime.signal])
|
|
211
|
+
: this.lifetime.signal;
|
|
212
|
+
}
|
|
213
|
+
track(operation) {
|
|
214
|
+
const settlement = Promise.resolve(operation).then(() => { }, () => { });
|
|
215
|
+
this.active.add(settlement);
|
|
216
|
+
void settlement.then(() => this.active.delete(settlement));
|
|
217
|
+
}
|
|
218
|
+
async finishClose() {
|
|
219
|
+
await Promise.allSettled([...this.streamClosers].map((close) => close()));
|
|
220
|
+
await Promise.allSettled([...this.active]);
|
|
221
|
+
await this.cleanup();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function drainIterator(iterator) {
|
|
225
|
+
if (!iterator.return)
|
|
226
|
+
return;
|
|
227
|
+
let result = await iterator.return(undefined);
|
|
228
|
+
while (!result.done)
|
|
229
|
+
result = await iterator.next();
|
|
230
|
+
}
|
|
34
231
|
/**
|
|
35
232
|
* Read preferences + run discovery once. Returned context drives the
|
|
36
233
|
* App's lifecycle decision: ready / picker / unhealthy.
|
|
@@ -47,7 +244,12 @@ export async function probeAgentSession() {
|
|
|
47
244
|
credentialGap: credentialGap(read.prefs, detected),
|
|
48
245
|
};
|
|
49
246
|
case 'missing':
|
|
50
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
preferences: null,
|
|
249
|
+
needsRepickReason: null,
|
|
250
|
+
detected,
|
|
251
|
+
credentialGap: null,
|
|
252
|
+
};
|
|
51
253
|
case 'needs-repick':
|
|
52
254
|
return {
|
|
53
255
|
preferences: null,
|
|
@@ -124,7 +326,10 @@ function buildToolRegistry(cwd) {
|
|
|
124
326
|
// why it sat on the no-prompt list; it writes into the user's project.
|
|
125
327
|
// Separate from the user-curated MEMORY.md that is injected into the prompt.
|
|
126
328
|
const memoryStore = new DiskMemoryStore({ baseDir: join(cwd, '.namzu') });
|
|
127
|
-
|
|
329
|
+
// Search through the store's async boundary. Its concrete index is lazy:
|
|
330
|
+
// handing `getIndex()` to the synchronous overload before the first store
|
|
331
|
+
// read makes a new process report every persisted memory as absent.
|
|
332
|
+
registry.register(buildMemoryTools(memoryStore));
|
|
128
333
|
// `search_tools` is deliberately NOT registered here. It is only useful
|
|
129
334
|
// where deferred tools exist, and that differs between this function's two
|
|
130
335
|
// callers: the session below passes a `taskStore`, so query() registers the
|
|
@@ -135,6 +340,13 @@ function buildToolRegistry(cwd) {
|
|
|
135
340
|
// caller's decision, made where the roster is known.
|
|
136
341
|
return { registry, memoryStore };
|
|
137
342
|
}
|
|
343
|
+
/** Refresh plugin skill metadata before each provider operation. */
|
|
344
|
+
async function currentPluginSkills(registry) {
|
|
345
|
+
const names = registry.list().map((skill) => skill.metadata.name);
|
|
346
|
+
for (const name of names)
|
|
347
|
+
await registry.load(name, 'metadata');
|
|
348
|
+
return registry.list();
|
|
349
|
+
}
|
|
138
350
|
export async function createAgentSession(prefs, detected, options = {}) {
|
|
139
351
|
const scope = options.scope ?? mintScope();
|
|
140
352
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -201,7 +413,9 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
201
413
|
'namzu.provider.skipped_count': fallbackPlan.notices.length,
|
|
202
414
|
});
|
|
203
415
|
for (const notice of fallbackPlan.notices) {
|
|
204
|
-
cliLogger().warn(notice, {
|
|
416
|
+
cliLogger().warn(notice, {
|
|
417
|
+
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.PROVIDER_RESOLVED,
|
|
418
|
+
});
|
|
205
419
|
}
|
|
206
420
|
let provider;
|
|
207
421
|
try {
|
|
@@ -218,29 +432,70 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
218
432
|
// changed. Gated on `det.oauth` so env / secrets credentials are never
|
|
219
433
|
// touched.
|
|
220
434
|
//
|
|
221
|
-
// `origin` decides WHICH
|
|
222
|
-
// discovery rather than being assumed here: namzu's own
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
const subscriptionRefresh = primary.id === 'anthropic' &&
|
|
435
|
+
// `origin` decides WHICH publication rule applies, and travels with the
|
|
436
|
+
// credential from discovery rather than being assumed here: namzu's own
|
|
437
|
+
// store can be conditionally replaced; the borrowed Keychain entry is
|
|
438
|
+
// read-only and a refreshed value may live only in this session.
|
|
439
|
+
const subscriptionRefresh = primary.id === 'anthropic' && det?.oauth?.origin === 'stored';
|
|
440
|
+
const borrowedAnthropic = primary.id === 'anthropic' &&
|
|
441
|
+
(det?.oauth?.origin === 'claude-file' || det?.oauth?.origin === 'keychain');
|
|
442
|
+
const borrowedCodexPath = primary.id === 'codex' && det?.source.kind === 'codex-file' ? det.source.path : undefined;
|
|
443
|
+
const storedCodex = primary.id === 'codex' && det?.codex?.origin === 'stored';
|
|
226
444
|
const credentialOrigin = det?.oauth?.origin ?? 'keychain';
|
|
227
445
|
let currentToken = det?.apiKey;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
446
|
+
let currentCodexAccount = det?.codex?.accountId;
|
|
447
|
+
let credentialTail = Promise.resolve();
|
|
448
|
+
let rejectedRefresh;
|
|
449
|
+
const performRefresh = async (signal) => {
|
|
450
|
+
signal?.throwIfAborted();
|
|
451
|
+
// Read only after this owner reaches the head. A sibling may have rotated
|
|
452
|
+
// the durable credential while we waited; reading before the queue would
|
|
453
|
+
// make its success invisible and permit a stale-token downgrade.
|
|
231
454
|
const cred = readSubscriptionCredential(credentialOrigin);
|
|
455
|
+
// This store is the authority for the whole session. Its absence is a
|
|
456
|
+
// credential withdrawal, not permission to keep using the client object
|
|
457
|
+
// that happens to remain in memory after logout or another process's clear.
|
|
232
458
|
if (!cred)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
459
|
+
throw new CredentialWithdrawnError();
|
|
460
|
+
if (rejectedRefresh && sameOAuthCredential(rejectedRefresh.credential, cred)) {
|
|
461
|
+
throw rejectedRefresh.error;
|
|
462
|
+
}
|
|
463
|
+
// A different non-null credential is an external winner (usually a fresh
|
|
464
|
+
// login). The old grant's refusal says nothing about this one.
|
|
465
|
+
rejectedRefresh = undefined;
|
|
466
|
+
let fresh;
|
|
467
|
+
try {
|
|
468
|
+
fresh = await ensureFreshAnthropicToken(cred.accessToken, {
|
|
469
|
+
refreshToken: cred.refreshToken,
|
|
470
|
+
expiresAt: cred.expiresAt,
|
|
471
|
+
scopes: cred.scopes,
|
|
472
|
+
origin: credentialOrigin,
|
|
473
|
+
}, signal);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
if (error instanceof CredentialRefreshRejectedError) {
|
|
477
|
+
// Cache only if the same credential is still authoritative after the
|
|
478
|
+
// network wait. A concurrent login must not inherit the old grant's
|
|
479
|
+
// permanent classification.
|
|
480
|
+
signal?.throwIfAborted();
|
|
481
|
+
const current = readSubscriptionCredential(credentialOrigin);
|
|
482
|
+
if (!current)
|
|
483
|
+
throw new CredentialWithdrawnError();
|
|
484
|
+
if (sameOAuthCredential(current, cred)) {
|
|
485
|
+
rejectedRefresh = { credential: cred, error };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
signal?.throwIfAborted();
|
|
239
491
|
if (fresh === currentToken)
|
|
240
492
|
return;
|
|
241
|
-
currentToken = fresh;
|
|
242
493
|
try {
|
|
243
|
-
|
|
494
|
+
const refreshedProvider = constructProvider('anthropic', { ...det, apiKey: fresh }, model);
|
|
495
|
+
// Publish the pair together. If construction fails, both old values
|
|
496
|
+
// remain live and the next operation can retry against the stored token.
|
|
497
|
+
provider = refreshedProvider;
|
|
498
|
+
currentToken = fresh;
|
|
244
499
|
}
|
|
245
500
|
catch (err) {
|
|
246
501
|
// Keep the previous client; the turn may still 401 but won't crash.
|
|
@@ -251,16 +506,93 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
251
506
|
cliLogger().warn('provider client rebuild after token refresh failed', exceptionAttributes(err));
|
|
252
507
|
}
|
|
253
508
|
};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
509
|
+
const rereadBorrowedCodex = (signal) => {
|
|
510
|
+
signal?.throwIfAborted();
|
|
511
|
+
if (!borrowedCodexPath)
|
|
512
|
+
return;
|
|
513
|
+
const credential = readCodexCredentialFile(borrowedCodexPath);
|
|
514
|
+
if (!credential) {
|
|
515
|
+
throw new CredentialWithdrawnError('The Codex session Namzu borrowed is no longer available. Run `codex login` or choose another provider.');
|
|
516
|
+
}
|
|
517
|
+
if (credential.expiresAt !== undefined && credential.expiresAt - Date.now() <= 60_000) {
|
|
518
|
+
throw new CredentialWithdrawnError('The Codex session on this device has expired. Run `codex login` to let its owner refresh the session, then retry in Namzu.');
|
|
519
|
+
}
|
|
520
|
+
if (credential.accessToken === currentToken && credential.accountId === currentCodexAccount) {
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const refreshedProvider = constructProvider('codex', {
|
|
524
|
+
...det,
|
|
525
|
+
apiKey: credential.accessToken,
|
|
526
|
+
codex: {
|
|
527
|
+
accountId: credential.accountId,
|
|
528
|
+
expiresAt: credential.expiresAt,
|
|
529
|
+
origin: 'codex-file',
|
|
530
|
+
},
|
|
531
|
+
}, model);
|
|
532
|
+
provider = refreshedProvider;
|
|
533
|
+
currentToken = credential.accessToken;
|
|
534
|
+
currentCodexAccount = credential.accountId;
|
|
535
|
+
};
|
|
536
|
+
const rereadBorrowedAnthropic = (signal) => {
|
|
537
|
+
signal?.throwIfAborted();
|
|
538
|
+
if (!borrowedAnthropic)
|
|
539
|
+
return;
|
|
540
|
+
const credential = readSubscriptionCredential(credentialOrigin);
|
|
541
|
+
if (!credential) {
|
|
542
|
+
throw new CredentialWithdrawnError('The Claude session Namzu borrowed is no longer available. Run `claude login` or choose another provider.');
|
|
543
|
+
}
|
|
544
|
+
if (credential.expiresAt !== undefined && credential.expiresAt - Date.now() <= 60_000) {
|
|
545
|
+
throw new CredentialWithdrawnError('The Claude session on this device has expired. Run `claude login` to let its owner refresh the session, then retry in Namzu.');
|
|
546
|
+
}
|
|
547
|
+
if (credential.accessToken === currentToken)
|
|
548
|
+
return;
|
|
549
|
+
provider = constructProvider('anthropic', { ...det, apiKey: credential.accessToken }, model);
|
|
550
|
+
currentToken = credential.accessToken;
|
|
551
|
+
};
|
|
552
|
+
const refreshStoredCodex = async (signal) => {
|
|
553
|
+
const credential = await ensureFreshStoredCodexCredential(signal);
|
|
554
|
+
if (credential.accessToken === currentToken && credential.accountId === currentCodexAccount) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const refreshedProvider = constructProvider('codex', {
|
|
558
|
+
...det,
|
|
559
|
+
apiKey: credential.accessToken,
|
|
560
|
+
codex: {
|
|
561
|
+
accountId: credential.accountId,
|
|
562
|
+
expiresAt: credential.expiresAt,
|
|
563
|
+
origin: 'stored',
|
|
564
|
+
},
|
|
565
|
+
}, model);
|
|
566
|
+
provider = refreshedProvider;
|
|
567
|
+
currentToken = credential.accessToken;
|
|
568
|
+
currentCodexAccount = credential.accountId;
|
|
569
|
+
};
|
|
570
|
+
const prepareProviderCredential = (signal) => {
|
|
571
|
+
if (!subscriptionRefresh && !borrowedAnthropic && !borrowedCodexPath && !storedCodex) {
|
|
572
|
+
signal?.throwIfAborted();
|
|
573
|
+
return Promise.resolve();
|
|
574
|
+
}
|
|
575
|
+
signal?.throwIfAborted();
|
|
576
|
+
const queued = credentialTail.then(async () => {
|
|
577
|
+
if (subscriptionRefresh)
|
|
578
|
+
await performRefresh(signal);
|
|
579
|
+
else if (borrowedAnthropic)
|
|
580
|
+
rereadBorrowedAnthropic(signal);
|
|
581
|
+
else if (storedCodex)
|
|
582
|
+
await refreshStoredCodex(signal);
|
|
583
|
+
else
|
|
584
|
+
rereadBorrowedCodex(signal);
|
|
585
|
+
});
|
|
586
|
+
// Keep later owners behind this slot even if its caller stops observing it.
|
|
587
|
+
// The catch makes the private tail non-rejecting without changing the
|
|
588
|
+
// exact outcome returned to the owner below.
|
|
589
|
+
credentialTail = queued.catch(() => { });
|
|
590
|
+
return signal ? observeWithSignal(queued, signal) : queued;
|
|
591
|
+
};
|
|
592
|
+
// Session-owned discovery with one drain cursor per run. A child shares the
|
|
593
|
+
// discovered scopes without being able to consume the parent's update, and
|
|
594
|
+
// an edit takes effect in this session rather than only after reconnecting.
|
|
595
|
+
const projectInstructions = new ProjectInstructionTracker(cwd);
|
|
264
596
|
// Before the registry, because a `requireIsolation` this machine cannot
|
|
265
597
|
// meet throws here — and failing before the session is built is the
|
|
266
598
|
// difference between "namzu refused to start" and a half-constructed
|
|
@@ -302,17 +634,23 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
302
634
|
'namzu.sandbox.unconfined': sandbox.unconfined,
|
|
303
635
|
});
|
|
304
636
|
const { registry, memoryStore } = buildToolRegistry(cwd);
|
|
637
|
+
// Registered only on the main session path. Sub-agents call
|
|
638
|
+
// `buildToolRegistry` directly below, so they never receive these tools.
|
|
639
|
+
// Per-send denial further keeps the schemas out of ordinary human turns.
|
|
640
|
+
const goalAuthorities = new Map();
|
|
641
|
+
const goalToolNames = new Set(SESSION_GOAL_TOOL_NAMES);
|
|
642
|
+
if (options.sessionGoals) {
|
|
643
|
+
registry.register(buildSessionGoalTools(options.sessionGoals, (runId) => goalAuthorities.get(runId)));
|
|
644
|
+
}
|
|
305
645
|
// External tool servers, before the roster is counted, so `toolNames` and
|
|
306
646
|
// the `/tools` list a user reads include what they configured. Connecting
|
|
307
647
|
// after the count would report a session smaller than the one that runs.
|
|
308
648
|
const mcp = await connectMcpServers(options.mcpServers, { cwd });
|
|
309
649
|
if (mcp.tools.length > 0)
|
|
310
650
|
registry.register([...mcp.tools]);
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
// yet, so a fabricated "plugins 0 · skills 0" here would claim a
|
|
315
|
-
// measurement that was never taken. This reports only what was.
|
|
651
|
+
// External connector discovery is reported separately from executable
|
|
652
|
+
// plugin discovery. Folding both counts together would make a failed server
|
|
653
|
+
// indistinguishable from a plugin that never enabled.
|
|
316
654
|
cliLogger().info('discovery complete', {
|
|
317
655
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.DISCOVERY_COMPLETED,
|
|
318
656
|
'namzu.discovery.kind': 'connector',
|
|
@@ -362,12 +700,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
362
700
|
const sub = await createSubagentRuntime({
|
|
363
701
|
cwd,
|
|
364
702
|
model,
|
|
703
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
704
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
705
|
+
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
706
|
+
: {}),
|
|
365
707
|
// A sub-agent works in the same repository and writes the same code,
|
|
366
708
|
// so it is bound by the same instructions. Without this the parent
|
|
367
709
|
// honours the project's rules and every task it delegates quietly
|
|
368
710
|
// does not — the worse half of the feature, because the delegating
|
|
369
711
|
// turn reports success either way.
|
|
370
|
-
|
|
712
|
+
projectInstructionContext: () => projectInstructions.createRunContext(),
|
|
371
713
|
// Same argument as the instructions, one step further: a sub-agent that
|
|
372
714
|
// does not know what day it is dates a changelog entry from a training
|
|
373
715
|
// cut-off, and the parent reports the delegation as successful.
|
|
@@ -447,6 +789,27 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
447
789
|
// settle — is what this repository has been doing all along by accident.
|
|
448
790
|
// A run that learned nothing still writes nothing.
|
|
449
791
|
const promoteMemory = createMemoryPromoter({ store: memoryStore });
|
|
792
|
+
// Plugins are the last fallible startup resource. The ordering is ownership:
|
|
793
|
+
// a malformed MCP entry cannot strand imported plugin hooks, and a plugin
|
|
794
|
+
// refusal closes the MCP processes already opened for this candidate before
|
|
795
|
+
// returning an inert session. Sub-agents were built above from their own
|
|
796
|
+
// registries, so executable plugins remain a top-level-session capability.
|
|
797
|
+
let pluginRuntime;
|
|
798
|
+
try {
|
|
799
|
+
pluginRuntime = await createCliPluginRuntime(options.plugins, registry, cwd);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
await mcp.close();
|
|
803
|
+
return emptySession(describeError(error));
|
|
804
|
+
}
|
|
805
|
+
if (pluginRuntime) {
|
|
806
|
+
cliLogger().info('discovery complete', {
|
|
807
|
+
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.DISCOVERY_COMPLETED,
|
|
808
|
+
'namzu.discovery.kind': 'plugin',
|
|
809
|
+
'namzu.discovery.count': pluginRuntime.pluginCount,
|
|
810
|
+
'namzu.discovery.skill_count': pluginRuntime.skills.size,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
450
813
|
// The one terminal POSITIVE event on this path, emitted exactly once —
|
|
451
814
|
// every early return above goes through `emptySession`, which emits
|
|
452
815
|
// `namzu.boot.refused` instead, and the `resolveSandbox` throw path above
|
|
@@ -457,109 +820,208 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
457
820
|
cliLogger().info('agent session ready', {
|
|
458
821
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.BOOT_READY,
|
|
459
822
|
});
|
|
823
|
+
const operations = new SessionOperationOwner(async () => {
|
|
824
|
+
const results = await Promise.allSettled([pluginRuntime?.close(), mcp.close()]);
|
|
825
|
+
const failures = results
|
|
826
|
+
.filter((result) => result.status === 'rejected')
|
|
827
|
+
.map((result) => result.reason);
|
|
828
|
+
if (failures.length > 0)
|
|
829
|
+
throw new AggregateError(failures, 'Session cleanup failed.');
|
|
830
|
+
});
|
|
831
|
+
let reasoningEffortLevels;
|
|
832
|
+
let effortNotice;
|
|
833
|
+
try {
|
|
834
|
+
const capabilityView = withProviderFallback([
|
|
835
|
+
{ provider, model },
|
|
836
|
+
...fallbackPlan.build(currentToken),
|
|
837
|
+
]);
|
|
838
|
+
const offered = capabilityView.reasoningEffortLevelsFor
|
|
839
|
+
? capabilityView.reasoningEffortLevelsFor(model)
|
|
840
|
+
: capabilityView.effortLevelsFor?.(model);
|
|
841
|
+
reasoningEffortLevels = offered === undefined ? undefined : Object.freeze([...offered]);
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
effortNotice = `Reasoning effort levels could not be established for this session: ${describeError(error)}.`;
|
|
845
|
+
}
|
|
460
846
|
return {
|
|
461
847
|
hasProvider: true,
|
|
848
|
+
sandbox: {
|
|
849
|
+
unconfined: sandbox.unconfined,
|
|
850
|
+
...(sandbox.environment ? { environment: sandbox.environment } : {}),
|
|
851
|
+
enforced: sandbox.enforced,
|
|
852
|
+
required: sandbox.required,
|
|
853
|
+
},
|
|
462
854
|
providerSummary: entry.label,
|
|
463
855
|
modelSummary: model,
|
|
856
|
+
reasoningEffortLevels,
|
|
857
|
+
compact: (messages) => operations.promise(undefined, async (signal) => {
|
|
858
|
+
await prepareProviderCredential(signal);
|
|
859
|
+
return compactNow({
|
|
860
|
+
messages,
|
|
861
|
+
config: COMPACTION_CONFIG,
|
|
862
|
+
provider,
|
|
863
|
+
model,
|
|
864
|
+
signal,
|
|
865
|
+
});
|
|
866
|
+
}),
|
|
464
867
|
// Reads the same registry object the deferred registration mutates, at
|
|
465
868
|
// call time — the pair of `promptExemptTools` below, and for the same
|
|
466
869
|
// reason.
|
|
467
|
-
toolNames: () => registry
|
|
870
|
+
toolNames: () => registry
|
|
871
|
+
.getCallableTools()
|
|
872
|
+
.map((t) => t.name)
|
|
873
|
+
.filter((name) => !goalToolNames.has(name)),
|
|
468
874
|
agentIds: allowedAgentIds,
|
|
469
|
-
instructionFiles
|
|
470
|
-
|
|
875
|
+
get instructionFiles() {
|
|
876
|
+
return projectInstructions.instructionFiles;
|
|
877
|
+
},
|
|
878
|
+
get skippedInstructionFiles() {
|
|
879
|
+
return projectInstructions.skippedInstructionFiles;
|
|
880
|
+
},
|
|
471
881
|
mcpConnected: mcp.connected,
|
|
472
882
|
mcpFailed: mcp.failed,
|
|
473
883
|
configNotices: [
|
|
474
884
|
...(capabilityNotice ? [capabilityNotice] : []),
|
|
885
|
+
...(effortNotice ? [effortNotice] : []),
|
|
475
886
|
...unresolvedNotice.map((line) => `Provider chain: capabilities could not be established for ${line}.`),
|
|
476
887
|
...fallbackPlan.notices,
|
|
477
888
|
],
|
|
478
|
-
close: () =>
|
|
889
|
+
close: () => operations.close(),
|
|
479
890
|
errorHint: null,
|
|
480
891
|
errorKind: null,
|
|
481
892
|
// Reads the same object the handler mutates, at call time.
|
|
482
893
|
approvalLatched: () => approval.all,
|
|
483
|
-
|
|
484
|
-
|
|
894
|
+
resetApprovalLatch: () => {
|
|
895
|
+
approval.all = false;
|
|
896
|
+
},
|
|
897
|
+
promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
|
|
898
|
+
send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
|
|
485
899
|
// Renew a lapsed OAuth token before the turn runs (no-op for valid
|
|
486
|
-
// tokens and non-
|
|
487
|
-
await
|
|
900
|
+
// tokens and non-subscription credentials).
|
|
901
|
+
await prepareProviderCredential(signal);
|
|
488
902
|
// namzu identity first (so it establishes who the agent is even when
|
|
489
903
|
// the credential layer prepends whatever prefix its token requires),
|
|
490
|
-
// then memory
|
|
491
|
-
//
|
|
904
|
+
// then memory and per-turn extra context. Project instructions are a
|
|
905
|
+
// separate retained user-context snapshot prepared by the controller;
|
|
906
|
+
// that is what lets nested scopes be replaced and persisted safely.
|
|
492
907
|
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
// whatever directory the agent was pointed at, so the rules they must
|
|
496
|
-
// not be able to rewrite are established before they are read.
|
|
497
|
-
//
|
|
498
|
-
// The environment block is read fresh every turn, unlike the project
|
|
499
|
-
// instructions, because both facts in it can change WHILE the session
|
|
908
|
+
// The environment block is read fresh every turn because both facts in it
|
|
909
|
+
// can change WHILE the session
|
|
500
910
|
// runs — midnight passes, and the agent checks out a branch itself.
|
|
501
911
|
// Its text only changes when a fact changes, so it costs a prompt-cache
|
|
502
912
|
// miss exactly when a hit would have been a stale claim.
|
|
913
|
+
const pluginSkills = pluginRuntime
|
|
914
|
+
? await currentPluginSkills(pluginRuntime.skills)
|
|
915
|
+
: undefined;
|
|
503
916
|
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
504
917
|
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
505
|
-
const systemPrompt = [
|
|
506
|
-
NAMZU_IDENTITY,
|
|
507
|
-
environmentPrompt,
|
|
508
|
-
memoryPrompt,
|
|
509
|
-
projectInstructions.prompt,
|
|
510
|
-
opts?.extraSystem,
|
|
511
|
-
]
|
|
918
|
+
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt, opts?.extraSystem]
|
|
512
919
|
.filter((s) => Boolean(s))
|
|
513
920
|
.join('\n\n') || undefined;
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
opts
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
921
|
+
let capturedAuthority;
|
|
922
|
+
if (opts?.goalRound) {
|
|
923
|
+
if (!opts.runId)
|
|
924
|
+
throw new Error('A goal round requires a caller-reserved runId.');
|
|
925
|
+
if (!options.sessionGoals)
|
|
926
|
+
throw new Error('This session has no durable goal store.');
|
|
927
|
+
if (opts.goalRound.sessionId !== scope.sessionId ||
|
|
928
|
+
opts.goalRound.tenantId !== scope.tenantId) {
|
|
929
|
+
throw new Error('Goal-round authority does not belong to this agent session scope.');
|
|
930
|
+
}
|
|
931
|
+
const current = await options.sessionGoals.getGoal(scope.sessionId, scope.tenantId);
|
|
932
|
+
if (!current ||
|
|
933
|
+
current.phase !== 'active' ||
|
|
934
|
+
current.id !== opts.goalRound.id ||
|
|
935
|
+
current.revision !== opts.goalRound.revision ||
|
|
936
|
+
current.objective !== opts.goalRound.objective ||
|
|
937
|
+
current.roundsAdmitted !== opts.goalRound.round ||
|
|
938
|
+
current.maxGoalRounds !== opts.goalRound.maxGoalRounds) {
|
|
939
|
+
throw new Error('Goal-round authority is stale or does not match the durable goal.');
|
|
940
|
+
}
|
|
941
|
+
if (goalAuthorities.has(opts.runId)) {
|
|
942
|
+
throw new Error(`Run ${opts.runId} already owns goal-round authority.`);
|
|
943
|
+
}
|
|
944
|
+
capturedAuthority = Object.freeze({ ...opts.goalRound });
|
|
945
|
+
goalAuthorities.set(opts.runId, capturedAuthority);
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
yield* runTurn({
|
|
949
|
+
provider,
|
|
950
|
+
// Constructed HERE, per turn, and that is not an optimisation to
|
|
951
|
+
// undo. `refreshTokenIfNeeded` above replaces the head's client
|
|
952
|
+
// object when an OAuth token rotates, so a member list built once at
|
|
953
|
+
// session creation would hand the kernel a client holding a token
|
|
954
|
+
// that expired hours ago — and a chain whose own members are stale
|
|
955
|
+
// is a fallback that fails for the reason the fallback exists to
|
|
956
|
+
// survive. Building a driver is a client object, not a request.
|
|
957
|
+
fallbackProviders: fallbackPlan.build(currentToken),
|
|
958
|
+
model,
|
|
959
|
+
tools: registry,
|
|
960
|
+
pluginManager: pluginRuntime?.manager,
|
|
961
|
+
skillRegistry: pluginRuntime?.skills,
|
|
962
|
+
skills: pluginSkills,
|
|
963
|
+
scope,
|
|
964
|
+
workingDirectory: cwd,
|
|
965
|
+
rules: options.rules,
|
|
966
|
+
permissionMode: opts?.permissionMode ?? options.permissionMode,
|
|
967
|
+
reviewAnswer: options.reviewAnswer,
|
|
968
|
+
maxAnswerReviews: options.maxAnswerReviews,
|
|
969
|
+
promoteMemory,
|
|
970
|
+
approval,
|
|
971
|
+
taskStore,
|
|
972
|
+
systemPrompt,
|
|
973
|
+
messages,
|
|
974
|
+
projectInstructionContext: projectInstructions.createRunContext(),
|
|
975
|
+
opts: { ...opts, signal },
|
|
976
|
+
taskGateway: subagentGateway,
|
|
977
|
+
onRunEvent: options.onRunEvent,
|
|
978
|
+
childSteps,
|
|
979
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
980
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
981
|
+
? {
|
|
982
|
+
sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs,
|
|
983
|
+
}
|
|
984
|
+
: {}),
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
finally {
|
|
988
|
+
if (opts?.runId &&
|
|
989
|
+
capturedAuthority &&
|
|
990
|
+
goalAuthorities.get(opts.runId) === capturedAuthority) {
|
|
991
|
+
goalAuthorities.delete(opts.runId);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
})()),
|
|
995
|
+
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => operations.promise(signal, async (ownedSignal) => {
|
|
545
996
|
// The same prelude a turn runs, and for the same reasons: a lapsed
|
|
546
997
|
// OAuth token has to be renewed before the provider is used, and the
|
|
547
998
|
// fallback chain has to be built AFTER that so its members do not
|
|
548
999
|
// hold a client the refresh just replaced.
|
|
549
|
-
await
|
|
1000
|
+
await prepareProviderCredential(ownedSignal);
|
|
1001
|
+
const pluginSkills = pluginRuntime
|
|
1002
|
+
? await currentPluginSkills(pluginRuntime.skills)
|
|
1003
|
+
: undefined;
|
|
550
1004
|
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
551
1005
|
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
552
|
-
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt
|
|
1006
|
+
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
|
|
553
1007
|
.filter((s) => Boolean(s))
|
|
554
1008
|
.join('\n\n') || undefined;
|
|
555
1009
|
return resumeRun({
|
|
556
1010
|
provider,
|
|
557
1011
|
fallbackProviders: fallbackPlan.build(currentToken),
|
|
558
1012
|
tools: registry,
|
|
1013
|
+
pluginManager: pluginRuntime?.manager,
|
|
1014
|
+
skillRegistry: pluginRuntime?.skills,
|
|
1015
|
+
skills: pluginSkills,
|
|
559
1016
|
taskStore,
|
|
560
1017
|
...(subagentGateway ? { taskGateway: subagentGateway } : {}),
|
|
561
1018
|
authorizationGate: gateFor(options.rules),
|
|
562
1019
|
compactionConfig: COMPACTION_CONFIG,
|
|
1020
|
+
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1021
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1022
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1023
|
+
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
1024
|
+
: {}),
|
|
563
1025
|
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
564
1026
|
// whose `attach` detaches whoever held it before, so a caller
|
|
565
1027
|
// resuming several runs in one process would leave only the last
|
|
@@ -585,7 +1047,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
585
1047
|
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
586
1048
|
// over a `RunEvent` and could not ask a tool anything.
|
|
587
1049
|
resumeHandler: makeResumeHandler(approval, undefined, options.permissionMode, (n, i) => isPromptExempt(registry, n, i)),
|
|
588
|
-
|
|
1050
|
+
signal: ownedSignal,
|
|
589
1051
|
// Attribution comes from the ENTRY, not from this session: the run
|
|
590
1052
|
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
591
1053
|
// it would file another tenant's work under this one.
|
|
@@ -600,7 +1062,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
600
1062
|
checkpointStore,
|
|
601
1063
|
...(claimFence !== undefined ? { claimFence } : {}),
|
|
602
1064
|
});
|
|
603
|
-
},
|
|
1065
|
+
}),
|
|
604
1066
|
};
|
|
605
1067
|
}
|
|
606
1068
|
function planFallbacks(members, detected) {
|
|
@@ -682,6 +1144,19 @@ export function constructProvider(id, det, model) {
|
|
|
682
1144
|
});
|
|
683
1145
|
return provider;
|
|
684
1146
|
}
|
|
1147
|
+
case 'codex': {
|
|
1148
|
+
if (!det?.codex?.accountId) {
|
|
1149
|
+
throw new Error('Codex subscription credentials require ChatGPT account routing. Sign in with Codex again or choose the OpenAI API-key provider.');
|
|
1150
|
+
}
|
|
1151
|
+
const { provider } = ProviderRegistry.create({
|
|
1152
|
+
type: 'codex',
|
|
1153
|
+
accessToken: det.apiKey ?? '',
|
|
1154
|
+
accountId: det.codex.accountId,
|
|
1155
|
+
baseURL: det.baseUrl,
|
|
1156
|
+
model,
|
|
1157
|
+
});
|
|
1158
|
+
return provider;
|
|
1159
|
+
}
|
|
685
1160
|
case 'deepseek': {
|
|
686
1161
|
const { provider } = ProviderRegistry.create({
|
|
687
1162
|
type: 'deepseek',
|
|
@@ -711,50 +1186,101 @@ export function constructProvider(id, det, model) {
|
|
|
711
1186
|
throw new Error(unsupportedProviderMessage(id));
|
|
712
1187
|
}
|
|
713
1188
|
}
|
|
1189
|
+
const PICKER_PROVIDER_DEADLINE_MS = 3_000;
|
|
1190
|
+
class PickerProviderTimeoutError extends Error {
|
|
1191
|
+
constructor() {
|
|
1192
|
+
super(`The provider did not answer within ${PICKER_PROVIDER_DEADLINE_MS}ms.`);
|
|
1193
|
+
this.name = 'PickerProviderTimeoutError';
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
/** Bound a picker side-call even when a third-party provider ignores abort. */
|
|
1197
|
+
async function runPickerProviderOperation(signal, operation) {
|
|
1198
|
+
signal?.throwIfAborted();
|
|
1199
|
+
const controller = new AbortController();
|
|
1200
|
+
const timeoutCause = new PickerProviderTimeoutError();
|
|
1201
|
+
let rejectBoundary = () => { };
|
|
1202
|
+
const boundary = new Promise((_resolve, reject) => {
|
|
1203
|
+
rejectBoundary = reject;
|
|
1204
|
+
});
|
|
1205
|
+
const onCallerAbort = () => {
|
|
1206
|
+
controller.abort(signal?.reason);
|
|
1207
|
+
rejectBoundary(signal?.reason);
|
|
1208
|
+
};
|
|
1209
|
+
signal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
1210
|
+
const timer = setTimeout(() => {
|
|
1211
|
+
controller.abort(timeoutCause);
|
|
1212
|
+
rejectBoundary(timeoutCause);
|
|
1213
|
+
}, PICKER_PROVIDER_DEADLINE_MS);
|
|
1214
|
+
try {
|
|
1215
|
+
return await Promise.race([operation(controller.signal), boundary]);
|
|
1216
|
+
}
|
|
1217
|
+
catch (error) {
|
|
1218
|
+
// Cooperative transports may replace the owner cause with AbortError.
|
|
1219
|
+
if (signal?.aborted)
|
|
1220
|
+
throw signal.reason;
|
|
1221
|
+
if (controller.signal.aborted && controller.signal.reason === timeoutCause)
|
|
1222
|
+
throw timeoutCause;
|
|
1223
|
+
throw error;
|
|
1224
|
+
}
|
|
1225
|
+
finally {
|
|
1226
|
+
clearTimeout(timer);
|
|
1227
|
+
signal?.removeEventListener('abort', onCallerAbort);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
714
1230
|
/**
|
|
715
1231
|
* Ask a detected provider what models it has.
|
|
716
1232
|
*
|
|
717
1233
|
* Instantiates the provider and calls its optional `listModels()`, inside a 3s
|
|
718
1234
|
* race so a wedged local server or a slow catalog cannot stall the UI.
|
|
719
1235
|
*/
|
|
720
|
-
export async function describeProviderModels(id, det) {
|
|
1236
|
+
export async function describeProviderModels(id, det, signal) {
|
|
721
1237
|
try {
|
|
1238
|
+
signal?.throwIfAborted();
|
|
722
1239
|
// constructProvider calls ProviderRegistry.create, which throws
|
|
723
1240
|
// "Unsupported provider type" until the vendor package has registered
|
|
724
1241
|
// itself. The run path registers lazily via ensureRegistered; the
|
|
725
1242
|
// listing path must do the same or every provider returns nothing.
|
|
726
1243
|
await ensureRegistered(id);
|
|
1244
|
+
signal?.throwIfAborted();
|
|
727
1245
|
const provider = constructProvider(id, det, det.entry.defaultModel);
|
|
728
1246
|
if (typeof provider.listModels !== 'function')
|
|
729
1247
|
return { kind: 'unsupported' };
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
return { kind: 'ok', models: models.map((m) => ({ id: m.id, name: m.name || m.id })) };
|
|
1248
|
+
const models = await runPickerProviderOperation(signal, (operationSignal) => provider.listModels?.(operationSignal) ?? Promise.resolve([]));
|
|
1249
|
+
return {
|
|
1250
|
+
kind: 'ok',
|
|
1251
|
+
models: models.map((m) => ({ id: m.id, name: m.name || m.id })),
|
|
1252
|
+
};
|
|
736
1253
|
}
|
|
737
1254
|
catch (err) {
|
|
738
|
-
|
|
1255
|
+
if (signal?.aborted)
|
|
1256
|
+
throw signal.reason;
|
|
1257
|
+
if (err instanceof PickerProviderTimeoutError)
|
|
1258
|
+
return { kind: 'timeout' };
|
|
1259
|
+
return {
|
|
1260
|
+
kind: 'failed',
|
|
1261
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1262
|
+
};
|
|
739
1263
|
}
|
|
740
1264
|
}
|
|
741
1265
|
/**
|
|
742
1266
|
* Check a key the operator just typed, without spending a turn.
|
|
743
1267
|
*
|
|
744
|
-
* Uses the provider's
|
|
745
|
-
* is proof the credential authenticates, and it costs nothing. A driver without
|
|
1268
|
+
* Uses the provider's declared `probeCredential` operation. A driver without
|
|
746
1269
|
* one cannot be checked cheaply, and that is reported as `unverifiable` rather
|
|
747
1270
|
* than dressed up as success — claiming a check that did not happen is the
|
|
748
|
-
* failure this whole surface is built to avoid.
|
|
1271
|
+
* failure this whole surface is built to avoid. A model catalogue is
|
|
1272
|
+
* deliberately not substituted: a menu is not evidence that a key worked.
|
|
749
1273
|
*
|
|
750
1274
|
* The key never appears in the returned reason. Provider errors are passed
|
|
751
1275
|
* through, and a driver that echoes a credential into its own error message
|
|
752
1276
|
* would defeat this; that is a driver bug and not one this can paper over, so
|
|
753
1277
|
* the reason is also truncated.
|
|
754
1278
|
*/
|
|
755
|
-
export async function verifyCredential(id, det) {
|
|
1279
|
+
export async function verifyCredential(id, det, signal) {
|
|
756
1280
|
try {
|
|
1281
|
+
signal?.throwIfAborted();
|
|
757
1282
|
await ensureRegistered(id);
|
|
1283
|
+
signal?.throwIfAborted();
|
|
758
1284
|
const provider = constructProvider(id, det, det.entry.defaultModel);
|
|
759
1285
|
// Declared, never inferred. A driver without a probe is unverifiable —
|
|
760
1286
|
// including one added years from now by someone who never reads this.
|
|
@@ -764,10 +1290,14 @@ export async function verifyCredential(id, det) {
|
|
|
764
1290
|
// not authenticate at all.
|
|
765
1291
|
if (typeof provider.probeCredential !== 'function')
|
|
766
1292
|
return { kind: 'unverifiable' };
|
|
767
|
-
await provider.probeCredential();
|
|
1293
|
+
await runPickerProviderOperation(signal, (operationSignal) => provider.probeCredential?.(operationSignal) ?? Promise.resolve());
|
|
768
1294
|
return { kind: 'verified' };
|
|
769
1295
|
}
|
|
770
1296
|
catch (err) {
|
|
1297
|
+
if (signal?.aborted)
|
|
1298
|
+
throw signal.reason;
|
|
1299
|
+
if (err instanceof PickerProviderTimeoutError)
|
|
1300
|
+
return { kind: 'unverifiable' };
|
|
771
1301
|
// The server answered and said no, versus nothing was learned. Collapsing
|
|
772
1302
|
// these would tell an operator on broken wifi to rotate a key that is fine.
|
|
773
1303
|
if (isCredentialRejection(err)) {
|
|
@@ -887,7 +1417,7 @@ const COMPACTION_CONFIG = {
|
|
|
887
1417
|
maxCharsPerRequirement: 300,
|
|
888
1418
|
maxCharsPerTask: 400,
|
|
889
1419
|
};
|
|
890
|
-
async function* runTurn({ provider, fallbackProviders, model, tools, scope, workingDirectory, rules, permissionMode, reviewAnswer, maxAnswerReviews, promoteMemory, approval, taskStore, systemPrompt, messages, opts, taskGateway, childSteps, sandboxProvider, onRunEvent, }) {
|
|
1420
|
+
async function* runTurn({ provider, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, workingDirectory, rules, permissionMode, reviewAnswer, maxAnswerReviews, promoteMemory, approval, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, childSteps, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
|
|
891
1421
|
const signal = opts?.signal;
|
|
892
1422
|
// One presenter for the whole stream, built from the registry this scope
|
|
893
1423
|
// already holds. Its absence HERE is what forced presentation to be name
|
|
@@ -897,11 +1427,18 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
897
1427
|
try {
|
|
898
1428
|
const events = query({
|
|
899
1429
|
provider,
|
|
1430
|
+
...(opts?.runId ? { runId: opts.runId } : {}),
|
|
900
1431
|
// Omitted rather than empty when there is no tail. `query` treats the
|
|
901
1432
|
// two the same, but an absent option reads as "this run has no chain"
|
|
902
1433
|
// where `[]` reads as "this run has a chain with nothing in it".
|
|
903
1434
|
...(fallbackProviders.length > 0 ? { fallbackProviders } : {}),
|
|
904
1435
|
tools,
|
|
1436
|
+
...(pluginManager ? { pluginManager } : {}),
|
|
1437
|
+
...(skillRegistry ? { skillRegistry } : {}),
|
|
1438
|
+
...(skills ? { skills } : {}),
|
|
1439
|
+
// Withheld at both provider and executor boundaries on every ordinary
|
|
1440
|
+
// turn. An admitted send owns the exact run-scoped authority above.
|
|
1441
|
+
...(!opts?.goalRound ? { deniedTools: SESSION_GOAL_TOOL_NAMES } : {}),
|
|
905
1442
|
taskStore,
|
|
906
1443
|
...(taskGateway ? { taskGateway } : {}),
|
|
907
1444
|
// `gateFor`, not the bare default: the default's `rules` is a hardcoded
|
|
@@ -909,6 +1446,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
909
1446
|
// path that runs every top-level turn. The sub-agent path called
|
|
910
1447
|
// `gateFor` and this one did not.
|
|
911
1448
|
...(sandboxProvider ? { sandboxProvider } : {}),
|
|
1449
|
+
...(sandboxTeardownTimeoutMs !== undefined ? { sandboxTeardownTimeoutMs } : {}),
|
|
912
1450
|
authorizationGate: gateFor(rules),
|
|
913
1451
|
compactionConfig: COMPACTION_CONFIG,
|
|
914
1452
|
// The CLI owns its process end to end, so it can safely hand the
|
|
@@ -917,6 +1455,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
917
1455
|
emergencySave: true,
|
|
918
1456
|
runConfig: {
|
|
919
1457
|
model,
|
|
1458
|
+
...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
|
|
920
1459
|
timeoutMs: 600_000,
|
|
921
1460
|
tokenBudget: 1_000_000,
|
|
922
1461
|
maxIterations: 50,
|
|
@@ -936,6 +1475,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
936
1475
|
agentId: 'namzu',
|
|
937
1476
|
agentName: 'namzu',
|
|
938
1477
|
...(systemPrompt ? { systemPrompt } : {}),
|
|
1478
|
+
projectInstructionContext,
|
|
939
1479
|
messages: [...messages],
|
|
940
1480
|
workingDirectory,
|
|
941
1481
|
// The exemption reads `tools` at decision time, so it sees the task
|
|
@@ -945,34 +1485,58 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
945
1485
|
signal,
|
|
946
1486
|
...scope,
|
|
947
1487
|
});
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
1488
|
+
let settled = false;
|
|
1489
|
+
try {
|
|
1490
|
+
while (true) {
|
|
1491
|
+
const next = await events.next();
|
|
1492
|
+
if (next.done) {
|
|
1493
|
+
settled = true;
|
|
1494
|
+
// A Run contains its fresh static/dynamic system floor as well as the
|
|
1495
|
+
// conversation. Only the latter crosses this host seam. Compaction
|
|
1496
|
+
// summaries survive because they ARE conversation state; arbitrary
|
|
1497
|
+
// system prompts are rebuilt fresh on every send and stay private to it.
|
|
1498
|
+
opts?.onConversationMessages?.(projectRunConversation(next.value.messages));
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const event = next.value;
|
|
1502
|
+
// Before the abort check and before `toAgentEvent`: a session
|
|
1503
|
+
// cancelled mid-turn still produced the events up to that point, and
|
|
1504
|
+
// they are the interesting ones. Every event, not just the ones the
|
|
1505
|
+
// TUI renders — an export that only saw what the screen showed would
|
|
1506
|
+
// be a recording of the interface rather than of the session.
|
|
1507
|
+
onRunEvent?.(event);
|
|
1508
|
+
if (signal?.aborted) {
|
|
1509
|
+
yield { kind: 'error', message: 'aborted' };
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const mapped = toAgentEvent(event, presenter);
|
|
1513
|
+
if (!mapped)
|
|
1514
|
+
continue;
|
|
1515
|
+
// On an `Agent` delegation finishing, attach the sub-agent's tool
|
|
1516
|
+
// steps (collected via the gateway while the call blocked) as a
|
|
1517
|
+
// `├─/└─` tree under its result, then reset for the next delegation.
|
|
1518
|
+
// (Don't clear on tool_executing: the parent's events can be buffered
|
|
1519
|
+
// and pulled only after the child already ran, which would wipe it.)
|
|
1520
|
+
if (event.type === 'tool_completed' &&
|
|
1521
|
+
event.toolName === 'Agent' &&
|
|
1522
|
+
childSteps.length > 0 &&
|
|
1523
|
+
mapped.kind === 'tool-end') {
|
|
1524
|
+
yield {
|
|
1525
|
+
...mapped,
|
|
1526
|
+
detail: [...(mapped.detail ?? []), ...asTree(childSteps)],
|
|
1527
|
+
};
|
|
1528
|
+
childSteps.length = 0;
|
|
1529
|
+
continue;
|
|
1530
|
+
}
|
|
1531
|
+
yield mapped;
|
|
974
1532
|
}
|
|
975
|
-
|
|
1533
|
+
}
|
|
1534
|
+
finally {
|
|
1535
|
+
// Manual iteration is what exposes the generator's Run return value.
|
|
1536
|
+
// Preserve `for await`'s other guarantee too: a consumer that stops
|
|
1537
|
+
// early must close the live query instead of abandoning its transport.
|
|
1538
|
+
if (!settled)
|
|
1539
|
+
await drainIterator(events);
|
|
976
1540
|
}
|
|
977
1541
|
}
|
|
978
1542
|
catch (err) {
|
|
@@ -1080,7 +1644,7 @@ presenter = GENERIC_PRESENTER) {
|
|
|
1080
1644
|
* survives the process, into the user's own repository, is not read-only under
|
|
1081
1645
|
* any reading, and it now prompts.
|
|
1082
1646
|
*/
|
|
1083
|
-
const PROMPT_EXEMPT_WRITES = new Set(['task_create', 'task_update']);
|
|
1647
|
+
const PROMPT_EXEMPT_WRITES = new Set(['task_create', 'task_update', 'update_goal']);
|
|
1084
1648
|
/**
|
|
1085
1649
|
* Whether a call runs without asking: it declares itself read-only, or it is a
|
|
1086
1650
|
* named exemption above.
|
|
@@ -1190,6 +1754,30 @@ export function toAgentEvent(event, presenter) {
|
|
|
1190
1754
|
};
|
|
1191
1755
|
case 'provider_fallback':
|
|
1192
1756
|
return { kind: 'provider-fallback', text: describeFallback(event) };
|
|
1757
|
+
case 'capability_warning':
|
|
1758
|
+
return {
|
|
1759
|
+
kind: 'capability-warning',
|
|
1760
|
+
capability: event.capability,
|
|
1761
|
+
text: event.message,
|
|
1762
|
+
};
|
|
1763
|
+
case 'message_history_repaired': {
|
|
1764
|
+
const changes = [
|
|
1765
|
+
event.duplicateToolResultsRemoved > 0
|
|
1766
|
+
? `${event.duplicateToolResultsRemoved} duplicate result${event.duplicateToolResultsRemoved === 1 ? '' : 's'} removed`
|
|
1767
|
+
: null,
|
|
1768
|
+
event.orphanedToolResultsRemoved > 0
|
|
1769
|
+
? `${event.orphanedToolResultsRemoved} orphaned result${event.orphanedToolResultsRemoved === 1 ? '' : 's'} removed`
|
|
1770
|
+
: null,
|
|
1771
|
+
event.syntheticToolResultsInserted > 0
|
|
1772
|
+
? `${event.syntheticToolResultsInserted} interrupted call${event.syntheticToolResultsInserted === 1 ? '' : 's'} closed with unknown outcome`
|
|
1773
|
+
: null,
|
|
1774
|
+
].filter((part) => part !== null);
|
|
1775
|
+
return {
|
|
1776
|
+
kind: 'history-repair',
|
|
1777
|
+
source: event.source,
|
|
1778
|
+
text: `Tool history repaired before the model call: ${changes.join('; ')}. Verify external state before retrying non-idempotent tools.`,
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1193
1781
|
case 'task_created':
|
|
1194
1782
|
return { kind: 'task', subject: event.subject, status: event.status };
|
|
1195
1783
|
case 'task_updated':
|
|
@@ -1204,7 +1792,10 @@ export function toAgentEvent(event, presenter) {
|
|
|
1204
1792
|
// timeout, a cancellation and a blocked output guardrail arrive. A
|
|
1205
1793
|
// consumer that reads this as success reports one for a run whose
|
|
1206
1794
|
// answer was refused.
|
|
1207
|
-
return {
|
|
1795
|
+
return {
|
|
1796
|
+
kind: 'done',
|
|
1797
|
+
...(event.stopReason ? { stopReason: event.stopReason } : {}),
|
|
1798
|
+
};
|
|
1208
1799
|
case 'run_failed':
|
|
1209
1800
|
// The classification is now carried on the event rather than
|
|
1210
1801
|
// having been flattened away upstream. Shown because "rate
|
|
@@ -1229,7 +1820,11 @@ export function toAgentEvent(event, presenter) {
|
|
|
1229
1820
|
shed: true,
|
|
1230
1821
|
};
|
|
1231
1822
|
case 'compaction_failed':
|
|
1232
|
-
return {
|
|
1823
|
+
return {
|
|
1824
|
+
kind: 'context',
|
|
1825
|
+
text: describeCompactionFailure(event),
|
|
1826
|
+
shed: false,
|
|
1827
|
+
};
|
|
1233
1828
|
default:
|
|
1234
1829
|
return null;
|
|
1235
1830
|
}
|
|
@@ -1333,8 +1928,14 @@ function asTree(steps) {
|
|
|
1333
1928
|
* the four deleted functions did for every tool.
|
|
1334
1929
|
*/
|
|
1335
1930
|
const GENERIC_PRESENTER = {
|
|
1336
|
-
presentCall: (_name, input) => ({
|
|
1337
|
-
|
|
1931
|
+
presentCall: (_name, input) => ({
|
|
1932
|
+
kind: 'generic',
|
|
1933
|
+
label: genericLabel(input),
|
|
1934
|
+
}),
|
|
1935
|
+
presentResult: (_name, _input, result) => ({
|
|
1936
|
+
kind: 'terminal',
|
|
1937
|
+
output: result.output ?? '',
|
|
1938
|
+
}),
|
|
1338
1939
|
};
|
|
1339
1940
|
/**
|
|
1340
1941
|
* The one presentation function this host keeps.
|
|
@@ -1513,7 +2114,9 @@ function logCapabilities(probes) {
|
|
|
1513
2114
|
const summary = probes
|
|
1514
2115
|
.map((p) => `${p.specifier.split('/').pop()} ${p.state === 'present' ? 'yes' : 'no'}`)
|
|
1515
2116
|
.join(' · ');
|
|
1516
|
-
log.info(summary, {
|
|
2117
|
+
log.info(summary, {
|
|
2118
|
+
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.CAPABILITY_DETECTED,
|
|
2119
|
+
});
|
|
1517
2120
|
for (const probe of probes) {
|
|
1518
2121
|
if (probe.state === 'broken') {
|
|
1519
2122
|
log.error('Capability probe failed to load', {
|
|
@@ -1545,6 +2148,15 @@ function emptySession(errorHint, errorKind = 'environment') {
|
|
|
1545
2148
|
});
|
|
1546
2149
|
return {
|
|
1547
2150
|
hasProvider: false,
|
|
2151
|
+
// A refused session ran nothing, so there is nothing confining anything.
|
|
2152
|
+
// Reporting an enforced control here would be a claim about a sandbox
|
|
2153
|
+
// this path never built.
|
|
2154
|
+
sandbox: { unconfined: true, enforced: [], required: [] },
|
|
2155
|
+
// No provider, so no summary can be built. Refusing is the honest
|
|
2156
|
+
// answer; returning `null` would say "nothing to shed".
|
|
2157
|
+
compact: async () => {
|
|
2158
|
+
throw new Error('No provider: pick one with /model before compacting.');
|
|
2159
|
+
},
|
|
1548
2160
|
errorKind,
|
|
1549
2161
|
providerSummary: null,
|
|
1550
2162
|
modelSummary: null,
|
|
@@ -1563,6 +2175,7 @@ function emptySession(errorHint, errorKind = 'environment') {
|
|
|
1563
2175
|
errorHint,
|
|
1564
2176
|
// No turn can run here, so no prompt can have been answered.
|
|
1565
2177
|
approvalLatched: () => false,
|
|
2178
|
+
resetApprovalLatch: () => { },
|
|
1566
2179
|
// No registry was built, so there is no roster to report on.
|
|
1567
2180
|
promptExemptTools: () => [],
|
|
1568
2181
|
send: async function* () {
|