@namzu/cli 12.1.0 → 13.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 +25 -1
- 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/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/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/credential-store.d.ts +17 -0
- package/dist/integrations/providers/credential-store.d.ts.map +1 -1
- package/dist/integrations/providers/credential-store.js +114 -2
- package/dist/integrations/providers/credential-store.js.map +1 -1
- package/dist/integrations/providers/index.d.ts +2 -2
- package/dist/integrations/providers/index.d.ts.map +1 -1
- package/dist/integrations/providers/index.js +2 -2
- 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 +45 -16
- package/dist/integrations/providers/oauth.d.ts.map +1 -1
- package/dist/integrations/providers/oauth.js +170 -43
- package/dist/integrations/providers/oauth.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 +1555 -143
- 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 +4 -4
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +86 -7
- 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 +665 -143
- 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/slashCommands.d.ts +178 -9
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +339 -15
- 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 +7 -7
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, ensureRegistered, findDetected, isAnthropicOAuthToken, isRegistered, missingCredentialMessage, primaryProvider, 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,65 @@ 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
|
-
//
|
|
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.
|
|
225
439
|
const subscriptionRefresh = primary.id === 'anthropic' && Boolean(det?.oauth);
|
|
226
440
|
const credentialOrigin = det?.oauth?.origin ?? 'keychain';
|
|
227
441
|
let currentToken = det?.apiKey;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
442
|
+
let refreshTail = Promise.resolve();
|
|
443
|
+
let rejectedRefresh;
|
|
444
|
+
const performRefresh = async (signal) => {
|
|
445
|
+
signal?.throwIfAborted();
|
|
446
|
+
// Read only after this owner reaches the head. A sibling may have rotated
|
|
447
|
+
// the durable credential while we waited; reading before the queue would
|
|
448
|
+
// make its success invisible and permit a stale-token downgrade.
|
|
231
449
|
const cred = readSubscriptionCredential(credentialOrigin);
|
|
450
|
+
// This store is the authority for the whole session. Its absence is a
|
|
451
|
+
// credential withdrawal, not permission to keep using the client object
|
|
452
|
+
// that happens to remain in memory after logout or another process's clear.
|
|
232
453
|
if (!cred)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
454
|
+
throw new CredentialWithdrawnError();
|
|
455
|
+
if (rejectedRefresh && sameOAuthCredential(rejectedRefresh.credential, cred)) {
|
|
456
|
+
throw rejectedRefresh.error;
|
|
457
|
+
}
|
|
458
|
+
// A different non-null credential is an external winner (usually a fresh
|
|
459
|
+
// login). The old grant's refusal says nothing about this one.
|
|
460
|
+
rejectedRefresh = undefined;
|
|
461
|
+
let fresh;
|
|
462
|
+
try {
|
|
463
|
+
fresh = await ensureFreshAnthropicToken(cred.accessToken, {
|
|
464
|
+
refreshToken: cred.refreshToken,
|
|
465
|
+
expiresAt: cred.expiresAt,
|
|
466
|
+
scopes: cred.scopes,
|
|
467
|
+
origin: credentialOrigin,
|
|
468
|
+
}, signal);
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
if (error instanceof CredentialRefreshRejectedError) {
|
|
472
|
+
// Cache only if the same credential is still authoritative after the
|
|
473
|
+
// network wait. A concurrent login must not inherit the old grant's
|
|
474
|
+
// permanent classification.
|
|
475
|
+
signal?.throwIfAborted();
|
|
476
|
+
const current = readSubscriptionCredential(credentialOrigin);
|
|
477
|
+
if (!current)
|
|
478
|
+
throw new CredentialWithdrawnError();
|
|
479
|
+
if (sameOAuthCredential(current, cred)) {
|
|
480
|
+
rejectedRefresh = { credential: cred, error };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
signal?.throwIfAborted();
|
|
239
486
|
if (fresh === currentToken)
|
|
240
487
|
return;
|
|
241
|
-
currentToken = fresh;
|
|
242
488
|
try {
|
|
243
|
-
|
|
489
|
+
const refreshedProvider = constructProvider('anthropic', { ...det, apiKey: fresh }, model);
|
|
490
|
+
// Publish the pair together. If construction fails, both old values
|
|
491
|
+
// remain live and the next operation can retry against the stored token.
|
|
492
|
+
provider = refreshedProvider;
|
|
493
|
+
currentToken = fresh;
|
|
244
494
|
}
|
|
245
495
|
catch (err) {
|
|
246
496
|
// Keep the previous client; the turn may still 401 but won't crash.
|
|
@@ -251,16 +501,23 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
251
501
|
cliLogger().warn('provider client rebuild after token refresh failed', exceptionAttributes(err));
|
|
252
502
|
}
|
|
253
503
|
};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
504
|
+
const refreshTokenIfNeeded = (signal) => {
|
|
505
|
+
if (!subscriptionRefresh) {
|
|
506
|
+
signal?.throwIfAborted();
|
|
507
|
+
return Promise.resolve();
|
|
508
|
+
}
|
|
509
|
+
signal?.throwIfAborted();
|
|
510
|
+
const queued = refreshTail.then(() => performRefresh(signal));
|
|
511
|
+
// Keep later owners behind this slot even if its caller stops observing it.
|
|
512
|
+
// The catch makes the private tail non-rejecting without changing the
|
|
513
|
+
// exact outcome returned to the owner below.
|
|
514
|
+
refreshTail = queued.catch(() => { });
|
|
515
|
+
return signal ? observeWithSignal(queued, signal) : queued;
|
|
516
|
+
};
|
|
517
|
+
// Session-owned discovery with one drain cursor per run. A child shares the
|
|
518
|
+
// discovered scopes without being able to consume the parent's update, and
|
|
519
|
+
// an edit takes effect in this session rather than only after reconnecting.
|
|
520
|
+
const projectInstructions = new ProjectInstructionTracker(cwd);
|
|
264
521
|
// Before the registry, because a `requireIsolation` this machine cannot
|
|
265
522
|
// meet throws here — and failing before the session is built is the
|
|
266
523
|
// difference between "namzu refused to start" and a half-constructed
|
|
@@ -302,17 +559,23 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
302
559
|
'namzu.sandbox.unconfined': sandbox.unconfined,
|
|
303
560
|
});
|
|
304
561
|
const { registry, memoryStore } = buildToolRegistry(cwd);
|
|
562
|
+
// Registered only on the main session path. Sub-agents call
|
|
563
|
+
// `buildToolRegistry` directly below, so they never receive these tools.
|
|
564
|
+
// Per-send denial further keeps the schemas out of ordinary human turns.
|
|
565
|
+
const goalAuthorities = new Map();
|
|
566
|
+
const goalToolNames = new Set(SESSION_GOAL_TOOL_NAMES);
|
|
567
|
+
if (options.sessionGoals) {
|
|
568
|
+
registry.register(buildSessionGoalTools(options.sessionGoals, (runId) => goalAuthorities.get(runId)));
|
|
569
|
+
}
|
|
305
570
|
// External tool servers, before the roster is counted, so `toolNames` and
|
|
306
571
|
// the `/tools` list a user reads include what they configured. Connecting
|
|
307
572
|
// after the count would report a session smaller than the one that runs.
|
|
308
573
|
const mcp = await connectMcpServers(options.mcpServers, { cwd });
|
|
309
574
|
if (mcp.tools.length > 0)
|
|
310
575
|
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.
|
|
576
|
+
// External connector discovery is reported separately from executable
|
|
577
|
+
// plugin discovery. Folding both counts together would make a failed server
|
|
578
|
+
// indistinguishable from a plugin that never enabled.
|
|
316
579
|
cliLogger().info('discovery complete', {
|
|
317
580
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.DISCOVERY_COMPLETED,
|
|
318
581
|
'namzu.discovery.kind': 'connector',
|
|
@@ -362,12 +625,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
362
625
|
const sub = await createSubagentRuntime({
|
|
363
626
|
cwd,
|
|
364
627
|
model,
|
|
628
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
629
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
630
|
+
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
631
|
+
: {}),
|
|
365
632
|
// A sub-agent works in the same repository and writes the same code,
|
|
366
633
|
// so it is bound by the same instructions. Without this the parent
|
|
367
634
|
// honours the project's rules and every task it delegates quietly
|
|
368
635
|
// does not — the worse half of the feature, because the delegating
|
|
369
636
|
// turn reports success either way.
|
|
370
|
-
|
|
637
|
+
projectInstructionContext: () => projectInstructions.createRunContext(),
|
|
371
638
|
// Same argument as the instructions, one step further: a sub-agent that
|
|
372
639
|
// does not know what day it is dates a changelog entry from a training
|
|
373
640
|
// cut-off, and the parent reports the delegation as successful.
|
|
@@ -447,6 +714,27 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
447
714
|
// settle — is what this repository has been doing all along by accident.
|
|
448
715
|
// A run that learned nothing still writes nothing.
|
|
449
716
|
const promoteMemory = createMemoryPromoter({ store: memoryStore });
|
|
717
|
+
// Plugins are the last fallible startup resource. The ordering is ownership:
|
|
718
|
+
// a malformed MCP entry cannot strand imported plugin hooks, and a plugin
|
|
719
|
+
// refusal closes the MCP processes already opened for this candidate before
|
|
720
|
+
// returning an inert session. Sub-agents were built above from their own
|
|
721
|
+
// registries, so executable plugins remain a top-level-session capability.
|
|
722
|
+
let pluginRuntime;
|
|
723
|
+
try {
|
|
724
|
+
pluginRuntime = await createCliPluginRuntime(options.plugins, registry, cwd);
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
await mcp.close();
|
|
728
|
+
return emptySession(describeError(error));
|
|
729
|
+
}
|
|
730
|
+
if (pluginRuntime) {
|
|
731
|
+
cliLogger().info('discovery complete', {
|
|
732
|
+
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.DISCOVERY_COMPLETED,
|
|
733
|
+
'namzu.discovery.kind': 'plugin',
|
|
734
|
+
'namzu.discovery.count': pluginRuntime.pluginCount,
|
|
735
|
+
'namzu.discovery.skill_count': pluginRuntime.skills.size,
|
|
736
|
+
});
|
|
737
|
+
}
|
|
450
738
|
// The one terminal POSITIVE event on this path, emitted exactly once —
|
|
451
739
|
// every early return above goes through `emptySession`, which emits
|
|
452
740
|
// `namzu.boot.refused` instead, and the `resolveSandbox` throw path above
|
|
@@ -457,109 +745,205 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
457
745
|
cliLogger().info('agent session ready', {
|
|
458
746
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.BOOT_READY,
|
|
459
747
|
});
|
|
748
|
+
const operations = new SessionOperationOwner(async () => {
|
|
749
|
+
const results = await Promise.allSettled([pluginRuntime?.close(), mcp.close()]);
|
|
750
|
+
const failures = results
|
|
751
|
+
.filter((result) => result.status === 'rejected')
|
|
752
|
+
.map((result) => result.reason);
|
|
753
|
+
if (failures.length > 0)
|
|
754
|
+
throw new AggregateError(failures, 'Session cleanup failed.');
|
|
755
|
+
});
|
|
756
|
+
let reasoningEffortLevels;
|
|
757
|
+
let effortNotice;
|
|
758
|
+
try {
|
|
759
|
+
const capabilityView = withProviderFallback([
|
|
760
|
+
{ provider, model },
|
|
761
|
+
...fallbackPlan.build(currentToken),
|
|
762
|
+
]);
|
|
763
|
+
const offered = capabilityView.reasoningEffortLevelsFor
|
|
764
|
+
? capabilityView.reasoningEffortLevelsFor(model)
|
|
765
|
+
: capabilityView.effortLevelsFor?.(model);
|
|
766
|
+
reasoningEffortLevels = offered === undefined ? undefined : Object.freeze([...offered]);
|
|
767
|
+
}
|
|
768
|
+
catch (error) {
|
|
769
|
+
effortNotice = `Reasoning effort levels could not be established for this session: ${describeError(error)}.`;
|
|
770
|
+
}
|
|
460
771
|
return {
|
|
461
772
|
hasProvider: true,
|
|
773
|
+
sandbox: {
|
|
774
|
+
unconfined: sandbox.unconfined,
|
|
775
|
+
...(sandbox.environment ? { environment: sandbox.environment } : {}),
|
|
776
|
+
enforced: sandbox.enforced,
|
|
777
|
+
required: sandbox.required,
|
|
778
|
+
},
|
|
462
779
|
providerSummary: entry.label,
|
|
463
780
|
modelSummary: model,
|
|
781
|
+
reasoningEffortLevels,
|
|
782
|
+
compact: (messages) => operations.promise(undefined, (signal) => compactNow({
|
|
783
|
+
messages,
|
|
784
|
+
config: COMPACTION_CONFIG,
|
|
785
|
+
provider,
|
|
786
|
+
model,
|
|
787
|
+
signal,
|
|
788
|
+
})),
|
|
464
789
|
// Reads the same registry object the deferred registration mutates, at
|
|
465
790
|
// call time — the pair of `promptExemptTools` below, and for the same
|
|
466
791
|
// reason.
|
|
467
|
-
toolNames: () => registry
|
|
792
|
+
toolNames: () => registry
|
|
793
|
+
.getCallableTools()
|
|
794
|
+
.map((t) => t.name)
|
|
795
|
+
.filter((name) => !goalToolNames.has(name)),
|
|
468
796
|
agentIds: allowedAgentIds,
|
|
469
|
-
instructionFiles
|
|
470
|
-
|
|
797
|
+
get instructionFiles() {
|
|
798
|
+
return projectInstructions.instructionFiles;
|
|
799
|
+
},
|
|
800
|
+
get skippedInstructionFiles() {
|
|
801
|
+
return projectInstructions.skippedInstructionFiles;
|
|
802
|
+
},
|
|
471
803
|
mcpConnected: mcp.connected,
|
|
472
804
|
mcpFailed: mcp.failed,
|
|
473
805
|
configNotices: [
|
|
474
806
|
...(capabilityNotice ? [capabilityNotice] : []),
|
|
807
|
+
...(effortNotice ? [effortNotice] : []),
|
|
475
808
|
...unresolvedNotice.map((line) => `Provider chain: capabilities could not be established for ${line}.`),
|
|
476
809
|
...fallbackPlan.notices,
|
|
477
810
|
],
|
|
478
|
-
close: () =>
|
|
811
|
+
close: () => operations.close(),
|
|
479
812
|
errorHint: null,
|
|
480
813
|
errorKind: null,
|
|
481
814
|
// Reads the same object the handler mutates, at call time.
|
|
482
815
|
approvalLatched: () => approval.all,
|
|
483
|
-
|
|
484
|
-
|
|
816
|
+
resetApprovalLatch: () => {
|
|
817
|
+
approval.all = false;
|
|
818
|
+
},
|
|
819
|
+
promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
|
|
820
|
+
send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
|
|
485
821
|
// Renew a lapsed OAuth token before the turn runs (no-op for valid
|
|
486
|
-
// tokens and non-
|
|
487
|
-
await refreshTokenIfNeeded();
|
|
822
|
+
// tokens and non-subscription credentials).
|
|
823
|
+
await refreshTokenIfNeeded(signal);
|
|
488
824
|
// namzu identity first (so it establishes who the agent is even when
|
|
489
825
|
// the credential layer prepends whatever prefix its token requires),
|
|
490
|
-
// then memory
|
|
491
|
-
//
|
|
826
|
+
// then memory and per-turn extra context. Project instructions are a
|
|
827
|
+
// separate retained user-context snapshot prepared by the controller;
|
|
828
|
+
// that is what lets nested scopes be replaced and persisted safely.
|
|
492
829
|
//
|
|
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
|
|
830
|
+
// The environment block is read fresh every turn because both facts in it
|
|
831
|
+
// can change WHILE the session
|
|
500
832
|
// runs — midnight passes, and the agent checks out a branch itself.
|
|
501
833
|
// Its text only changes when a fact changes, so it costs a prompt-cache
|
|
502
834
|
// miss exactly when a hit would have been a stale claim.
|
|
835
|
+
const pluginSkills = pluginRuntime
|
|
836
|
+
? await currentPluginSkills(pluginRuntime.skills)
|
|
837
|
+
: undefined;
|
|
503
838
|
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
504
839
|
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
505
|
-
const systemPrompt = [
|
|
506
|
-
NAMZU_IDENTITY,
|
|
507
|
-
environmentPrompt,
|
|
508
|
-
memoryPrompt,
|
|
509
|
-
projectInstructions.prompt,
|
|
510
|
-
opts?.extraSystem,
|
|
511
|
-
]
|
|
840
|
+
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt, opts?.extraSystem]
|
|
512
841
|
.filter((s) => Boolean(s))
|
|
513
842
|
.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
|
-
|
|
843
|
+
let capturedAuthority;
|
|
844
|
+
if (opts?.goalRound) {
|
|
845
|
+
if (!opts.runId)
|
|
846
|
+
throw new Error('A goal round requires a caller-reserved runId.');
|
|
847
|
+
if (!options.sessionGoals)
|
|
848
|
+
throw new Error('This session has no durable goal store.');
|
|
849
|
+
if (opts.goalRound.sessionId !== scope.sessionId ||
|
|
850
|
+
opts.goalRound.tenantId !== scope.tenantId) {
|
|
851
|
+
throw new Error('Goal-round authority does not belong to this agent session scope.');
|
|
852
|
+
}
|
|
853
|
+
const current = await options.sessionGoals.getGoal(scope.sessionId, scope.tenantId);
|
|
854
|
+
if (!current ||
|
|
855
|
+
current.phase !== 'active' ||
|
|
856
|
+
current.id !== opts.goalRound.id ||
|
|
857
|
+
current.revision !== opts.goalRound.revision ||
|
|
858
|
+
current.objective !== opts.goalRound.objective ||
|
|
859
|
+
current.roundsAdmitted !== opts.goalRound.round ||
|
|
860
|
+
current.maxGoalRounds !== opts.goalRound.maxGoalRounds) {
|
|
861
|
+
throw new Error('Goal-round authority is stale or does not match the durable goal.');
|
|
862
|
+
}
|
|
863
|
+
if (goalAuthorities.has(opts.runId)) {
|
|
864
|
+
throw new Error(`Run ${opts.runId} already owns goal-round authority.`);
|
|
865
|
+
}
|
|
866
|
+
capturedAuthority = Object.freeze({ ...opts.goalRound });
|
|
867
|
+
goalAuthorities.set(opts.runId, capturedAuthority);
|
|
868
|
+
}
|
|
869
|
+
try {
|
|
870
|
+
yield* runTurn({
|
|
871
|
+
provider,
|
|
872
|
+
// Constructed HERE, per turn, and that is not an optimisation to
|
|
873
|
+
// undo. `refreshTokenIfNeeded` above replaces the head's client
|
|
874
|
+
// object when an OAuth token rotates, so a member list built once at
|
|
875
|
+
// session creation would hand the kernel a client holding a token
|
|
876
|
+
// that expired hours ago — and a chain whose own members are stale
|
|
877
|
+
// is a fallback that fails for the reason the fallback exists to
|
|
878
|
+
// survive. Building a driver is a client object, not a request.
|
|
879
|
+
fallbackProviders: fallbackPlan.build(currentToken),
|
|
880
|
+
model,
|
|
881
|
+
tools: registry,
|
|
882
|
+
pluginManager: pluginRuntime?.manager,
|
|
883
|
+
skillRegistry: pluginRuntime?.skills,
|
|
884
|
+
skills: pluginSkills,
|
|
885
|
+
scope,
|
|
886
|
+
workingDirectory: cwd,
|
|
887
|
+
rules: options.rules,
|
|
888
|
+
permissionMode: opts?.permissionMode ?? options.permissionMode,
|
|
889
|
+
reviewAnswer: options.reviewAnswer,
|
|
890
|
+
maxAnswerReviews: options.maxAnswerReviews,
|
|
891
|
+
promoteMemory,
|
|
892
|
+
approval,
|
|
893
|
+
taskStore,
|
|
894
|
+
systemPrompt,
|
|
895
|
+
messages,
|
|
896
|
+
projectInstructionContext: projectInstructions.createRunContext(),
|
|
897
|
+
opts: { ...opts, signal },
|
|
898
|
+
taskGateway: subagentGateway,
|
|
899
|
+
onRunEvent: options.onRunEvent,
|
|
900
|
+
childSteps,
|
|
901
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
902
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
903
|
+
? {
|
|
904
|
+
sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs,
|
|
905
|
+
}
|
|
906
|
+
: {}),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
finally {
|
|
910
|
+
if (opts?.runId &&
|
|
911
|
+
capturedAuthority &&
|
|
912
|
+
goalAuthorities.get(opts.runId) === capturedAuthority) {
|
|
913
|
+
goalAuthorities.delete(opts.runId);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
})()),
|
|
917
|
+
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => operations.promise(signal, async (ownedSignal) => {
|
|
545
918
|
// The same prelude a turn runs, and for the same reasons: a lapsed
|
|
546
919
|
// OAuth token has to be renewed before the provider is used, and the
|
|
547
920
|
// fallback chain has to be built AFTER that so its members do not
|
|
548
921
|
// hold a client the refresh just replaced.
|
|
549
|
-
await refreshTokenIfNeeded();
|
|
922
|
+
await refreshTokenIfNeeded(ownedSignal);
|
|
923
|
+
const pluginSkills = pluginRuntime
|
|
924
|
+
? await currentPluginSkills(pluginRuntime.skills)
|
|
925
|
+
: undefined;
|
|
550
926
|
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
551
927
|
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
552
|
-
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt
|
|
928
|
+
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
|
|
553
929
|
.filter((s) => Boolean(s))
|
|
554
930
|
.join('\n\n') || undefined;
|
|
555
931
|
return resumeRun({
|
|
556
932
|
provider,
|
|
557
933
|
fallbackProviders: fallbackPlan.build(currentToken),
|
|
558
934
|
tools: registry,
|
|
935
|
+
pluginManager: pluginRuntime?.manager,
|
|
936
|
+
skillRegistry: pluginRuntime?.skills,
|
|
937
|
+
skills: pluginSkills,
|
|
559
938
|
taskStore,
|
|
560
939
|
...(subagentGateway ? { taskGateway: subagentGateway } : {}),
|
|
561
940
|
authorizationGate: gateFor(options.rules),
|
|
562
941
|
compactionConfig: COMPACTION_CONFIG,
|
|
942
|
+
projectInstructionContext: projectInstructions.createRunContext(),
|
|
943
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
944
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
945
|
+
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
946
|
+
: {}),
|
|
563
947
|
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
564
948
|
// whose `attach` detaches whoever held it before, so a caller
|
|
565
949
|
// resuming several runs in one process would leave only the last
|
|
@@ -585,7 +969,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
585
969
|
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
586
970
|
// over a `RunEvent` and could not ask a tool anything.
|
|
587
971
|
resumeHandler: makeResumeHandler(approval, undefined, options.permissionMode, (n, i) => isPromptExempt(registry, n, i)),
|
|
588
|
-
|
|
972
|
+
signal: ownedSignal,
|
|
589
973
|
// Attribution comes from the ENTRY, not from this session: the run
|
|
590
974
|
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
591
975
|
// it would file another tenant's work under this one.
|
|
@@ -600,7 +984,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
600
984
|
checkpointStore,
|
|
601
985
|
...(claimFence !== undefined ? { claimFence } : {}),
|
|
602
986
|
});
|
|
603
|
-
},
|
|
987
|
+
}),
|
|
604
988
|
};
|
|
605
989
|
}
|
|
606
990
|
function planFallbacks(members, detected) {
|
|
@@ -711,50 +1095,101 @@ export function constructProvider(id, det, model) {
|
|
|
711
1095
|
throw new Error(unsupportedProviderMessage(id));
|
|
712
1096
|
}
|
|
713
1097
|
}
|
|
1098
|
+
const PICKER_PROVIDER_DEADLINE_MS = 3_000;
|
|
1099
|
+
class PickerProviderTimeoutError extends Error {
|
|
1100
|
+
constructor() {
|
|
1101
|
+
super(`The provider did not answer within ${PICKER_PROVIDER_DEADLINE_MS}ms.`);
|
|
1102
|
+
this.name = 'PickerProviderTimeoutError';
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
/** Bound a picker side-call even when a third-party provider ignores abort. */
|
|
1106
|
+
async function runPickerProviderOperation(signal, operation) {
|
|
1107
|
+
signal?.throwIfAborted();
|
|
1108
|
+
const controller = new AbortController();
|
|
1109
|
+
const timeoutCause = new PickerProviderTimeoutError();
|
|
1110
|
+
let rejectBoundary = () => { };
|
|
1111
|
+
const boundary = new Promise((_resolve, reject) => {
|
|
1112
|
+
rejectBoundary = reject;
|
|
1113
|
+
});
|
|
1114
|
+
const onCallerAbort = () => {
|
|
1115
|
+
controller.abort(signal?.reason);
|
|
1116
|
+
rejectBoundary(signal?.reason);
|
|
1117
|
+
};
|
|
1118
|
+
signal?.addEventListener('abort', onCallerAbort, { once: true });
|
|
1119
|
+
const timer = setTimeout(() => {
|
|
1120
|
+
controller.abort(timeoutCause);
|
|
1121
|
+
rejectBoundary(timeoutCause);
|
|
1122
|
+
}, PICKER_PROVIDER_DEADLINE_MS);
|
|
1123
|
+
try {
|
|
1124
|
+
return await Promise.race([operation(controller.signal), boundary]);
|
|
1125
|
+
}
|
|
1126
|
+
catch (error) {
|
|
1127
|
+
// Cooperative transports may replace the owner cause with AbortError.
|
|
1128
|
+
if (signal?.aborted)
|
|
1129
|
+
throw signal.reason;
|
|
1130
|
+
if (controller.signal.aborted && controller.signal.reason === timeoutCause)
|
|
1131
|
+
throw timeoutCause;
|
|
1132
|
+
throw error;
|
|
1133
|
+
}
|
|
1134
|
+
finally {
|
|
1135
|
+
clearTimeout(timer);
|
|
1136
|
+
signal?.removeEventListener('abort', onCallerAbort);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
714
1139
|
/**
|
|
715
1140
|
* Ask a detected provider what models it has.
|
|
716
1141
|
*
|
|
717
1142
|
* Instantiates the provider and calls its optional `listModels()`, inside a 3s
|
|
718
1143
|
* race so a wedged local server or a slow catalog cannot stall the UI.
|
|
719
1144
|
*/
|
|
720
|
-
export async function describeProviderModels(id, det) {
|
|
1145
|
+
export async function describeProviderModels(id, det, signal) {
|
|
721
1146
|
try {
|
|
1147
|
+
signal?.throwIfAborted();
|
|
722
1148
|
// constructProvider calls ProviderRegistry.create, which throws
|
|
723
1149
|
// "Unsupported provider type" until the vendor package has registered
|
|
724
1150
|
// itself. The run path registers lazily via ensureRegistered; the
|
|
725
1151
|
// listing path must do the same or every provider returns nothing.
|
|
726
1152
|
await ensureRegistered(id);
|
|
1153
|
+
signal?.throwIfAborted();
|
|
727
1154
|
const provider = constructProvider(id, det, det.entry.defaultModel);
|
|
728
1155
|
if (typeof provider.listModels !== 'function')
|
|
729
1156
|
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 })) };
|
|
1157
|
+
const models = await runPickerProviderOperation(signal, (operationSignal) => provider.listModels?.(operationSignal) ?? Promise.resolve([]));
|
|
1158
|
+
return {
|
|
1159
|
+
kind: 'ok',
|
|
1160
|
+
models: models.map((m) => ({ id: m.id, name: m.name || m.id })),
|
|
1161
|
+
};
|
|
736
1162
|
}
|
|
737
1163
|
catch (err) {
|
|
738
|
-
|
|
1164
|
+
if (signal?.aborted)
|
|
1165
|
+
throw signal.reason;
|
|
1166
|
+
if (err instanceof PickerProviderTimeoutError)
|
|
1167
|
+
return { kind: 'timeout' };
|
|
1168
|
+
return {
|
|
1169
|
+
kind: 'failed',
|
|
1170
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
1171
|
+
};
|
|
739
1172
|
}
|
|
740
1173
|
}
|
|
741
1174
|
/**
|
|
742
1175
|
* Check a key the operator just typed, without spending a turn.
|
|
743
1176
|
*
|
|
744
|
-
* Uses the provider's
|
|
745
|
-
* is proof the credential authenticates, and it costs nothing. A driver without
|
|
1177
|
+
* Uses the provider's declared `probeCredential` operation. A driver without
|
|
746
1178
|
* one cannot be checked cheaply, and that is reported as `unverifiable` rather
|
|
747
1179
|
* than dressed up as success — claiming a check that did not happen is the
|
|
748
|
-
* failure this whole surface is built to avoid.
|
|
1180
|
+
* failure this whole surface is built to avoid. A model catalogue is
|
|
1181
|
+
* deliberately not substituted: a menu is not evidence that a key worked.
|
|
749
1182
|
*
|
|
750
1183
|
* The key never appears in the returned reason. Provider errors are passed
|
|
751
1184
|
* through, and a driver that echoes a credential into its own error message
|
|
752
1185
|
* would defeat this; that is a driver bug and not one this can paper over, so
|
|
753
1186
|
* the reason is also truncated.
|
|
754
1187
|
*/
|
|
755
|
-
export async function verifyCredential(id, det) {
|
|
1188
|
+
export async function verifyCredential(id, det, signal) {
|
|
756
1189
|
try {
|
|
1190
|
+
signal?.throwIfAborted();
|
|
757
1191
|
await ensureRegistered(id);
|
|
1192
|
+
signal?.throwIfAborted();
|
|
758
1193
|
const provider = constructProvider(id, det, det.entry.defaultModel);
|
|
759
1194
|
// Declared, never inferred. A driver without a probe is unverifiable —
|
|
760
1195
|
// including one added years from now by someone who never reads this.
|
|
@@ -764,10 +1199,14 @@ export async function verifyCredential(id, det) {
|
|
|
764
1199
|
// not authenticate at all.
|
|
765
1200
|
if (typeof provider.probeCredential !== 'function')
|
|
766
1201
|
return { kind: 'unverifiable' };
|
|
767
|
-
await provider.probeCredential();
|
|
1202
|
+
await runPickerProviderOperation(signal, (operationSignal) => provider.probeCredential?.(operationSignal) ?? Promise.resolve());
|
|
768
1203
|
return { kind: 'verified' };
|
|
769
1204
|
}
|
|
770
1205
|
catch (err) {
|
|
1206
|
+
if (signal?.aborted)
|
|
1207
|
+
throw signal.reason;
|
|
1208
|
+
if (err instanceof PickerProviderTimeoutError)
|
|
1209
|
+
return { kind: 'unverifiable' };
|
|
771
1210
|
// The server answered and said no, versus nothing was learned. Collapsing
|
|
772
1211
|
// these would tell an operator on broken wifi to rotate a key that is fine.
|
|
773
1212
|
if (isCredentialRejection(err)) {
|
|
@@ -887,7 +1326,7 @@ const COMPACTION_CONFIG = {
|
|
|
887
1326
|
maxCharsPerRequirement: 300,
|
|
888
1327
|
maxCharsPerTask: 400,
|
|
889
1328
|
};
|
|
890
|
-
async function* runTurn({ provider, fallbackProviders, model, tools, scope, workingDirectory, rules, permissionMode, reviewAnswer, maxAnswerReviews, promoteMemory, approval, taskStore, systemPrompt, messages, opts, taskGateway, childSteps, sandboxProvider, onRunEvent, }) {
|
|
1329
|
+
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
1330
|
const signal = opts?.signal;
|
|
892
1331
|
// One presenter for the whole stream, built from the registry this scope
|
|
893
1332
|
// already holds. Its absence HERE is what forced presentation to be name
|
|
@@ -897,11 +1336,18 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
897
1336
|
try {
|
|
898
1337
|
const events = query({
|
|
899
1338
|
provider,
|
|
1339
|
+
...(opts?.runId ? { runId: opts.runId } : {}),
|
|
900
1340
|
// Omitted rather than empty when there is no tail. `query` treats the
|
|
901
1341
|
// two the same, but an absent option reads as "this run has no chain"
|
|
902
1342
|
// where `[]` reads as "this run has a chain with nothing in it".
|
|
903
1343
|
...(fallbackProviders.length > 0 ? { fallbackProviders } : {}),
|
|
904
1344
|
tools,
|
|
1345
|
+
...(pluginManager ? { pluginManager } : {}),
|
|
1346
|
+
...(skillRegistry ? { skillRegistry } : {}),
|
|
1347
|
+
...(skills ? { skills } : {}),
|
|
1348
|
+
// Withheld at both provider and executor boundaries on every ordinary
|
|
1349
|
+
// turn. An admitted send owns the exact run-scoped authority above.
|
|
1350
|
+
...(!opts?.goalRound ? { deniedTools: SESSION_GOAL_TOOL_NAMES } : {}),
|
|
905
1351
|
taskStore,
|
|
906
1352
|
...(taskGateway ? { taskGateway } : {}),
|
|
907
1353
|
// `gateFor`, not the bare default: the default's `rules` is a hardcoded
|
|
@@ -909,6 +1355,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
909
1355
|
// path that runs every top-level turn. The sub-agent path called
|
|
910
1356
|
// `gateFor` and this one did not.
|
|
911
1357
|
...(sandboxProvider ? { sandboxProvider } : {}),
|
|
1358
|
+
...(sandboxTeardownTimeoutMs !== undefined ? { sandboxTeardownTimeoutMs } : {}),
|
|
912
1359
|
authorizationGate: gateFor(rules),
|
|
913
1360
|
compactionConfig: COMPACTION_CONFIG,
|
|
914
1361
|
// The CLI owns its process end to end, so it can safely hand the
|
|
@@ -917,6 +1364,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
917
1364
|
emergencySave: true,
|
|
918
1365
|
runConfig: {
|
|
919
1366
|
model,
|
|
1367
|
+
...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
|
|
920
1368
|
timeoutMs: 600_000,
|
|
921
1369
|
tokenBudget: 1_000_000,
|
|
922
1370
|
maxIterations: 50,
|
|
@@ -936,6 +1384,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
936
1384
|
agentId: 'namzu',
|
|
937
1385
|
agentName: 'namzu',
|
|
938
1386
|
...(systemPrompt ? { systemPrompt } : {}),
|
|
1387
|
+
projectInstructionContext,
|
|
939
1388
|
messages: [...messages],
|
|
940
1389
|
workingDirectory,
|
|
941
1390
|
// The exemption reads `tools` at decision time, so it sees the task
|
|
@@ -945,34 +1394,58 @@ async function* runTurn({ provider, fallbackProviders, model, tools, scope, work
|
|
|
945
1394
|
signal,
|
|
946
1395
|
...scope,
|
|
947
1396
|
});
|
|
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
|
-
|
|
1397
|
+
let settled = false;
|
|
1398
|
+
try {
|
|
1399
|
+
while (true) {
|
|
1400
|
+
const next = await events.next();
|
|
1401
|
+
if (next.done) {
|
|
1402
|
+
settled = true;
|
|
1403
|
+
// A Run contains its fresh static/dynamic system floor as well as the
|
|
1404
|
+
// conversation. Only the latter crosses this host seam. Compaction
|
|
1405
|
+
// summaries survive because they ARE conversation state; arbitrary
|
|
1406
|
+
// system prompts are rebuilt fresh on every send and stay private to it.
|
|
1407
|
+
opts?.onConversationMessages?.(projectRunConversation(next.value.messages));
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const event = next.value;
|
|
1411
|
+
// Before the abort check and before `toAgentEvent`: a session
|
|
1412
|
+
// cancelled mid-turn still produced the events up to that point, and
|
|
1413
|
+
// they are the interesting ones. Every event, not just the ones the
|
|
1414
|
+
// TUI renders — an export that only saw what the screen showed would
|
|
1415
|
+
// be a recording of the interface rather than of the session.
|
|
1416
|
+
onRunEvent?.(event);
|
|
1417
|
+
if (signal?.aborted) {
|
|
1418
|
+
yield { kind: 'error', message: 'aborted' };
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
const mapped = toAgentEvent(event, presenter);
|
|
1422
|
+
if (!mapped)
|
|
1423
|
+
continue;
|
|
1424
|
+
// On an `Agent` delegation finishing, attach the sub-agent's tool
|
|
1425
|
+
// steps (collected via the gateway while the call blocked) as a
|
|
1426
|
+
// `├─/└─` tree under its result, then reset for the next delegation.
|
|
1427
|
+
// (Don't clear on tool_executing: the parent's events can be buffered
|
|
1428
|
+
// and pulled only after the child already ran, which would wipe it.)
|
|
1429
|
+
if (event.type === 'tool_completed' &&
|
|
1430
|
+
event.toolName === 'Agent' &&
|
|
1431
|
+
childSteps.length > 0 &&
|
|
1432
|
+
mapped.kind === 'tool-end') {
|
|
1433
|
+
yield {
|
|
1434
|
+
...mapped,
|
|
1435
|
+
detail: [...(mapped.detail ?? []), ...asTree(childSteps)],
|
|
1436
|
+
};
|
|
1437
|
+
childSteps.length = 0;
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
yield mapped;
|
|
974
1441
|
}
|
|
975
|
-
|
|
1442
|
+
}
|
|
1443
|
+
finally {
|
|
1444
|
+
// Manual iteration is what exposes the generator's Run return value.
|
|
1445
|
+
// Preserve `for await`'s other guarantee too: a consumer that stops
|
|
1446
|
+
// early must close the live query instead of abandoning its transport.
|
|
1447
|
+
if (!settled)
|
|
1448
|
+
await drainIterator(events);
|
|
976
1449
|
}
|
|
977
1450
|
}
|
|
978
1451
|
catch (err) {
|
|
@@ -1080,7 +1553,7 @@ presenter = GENERIC_PRESENTER) {
|
|
|
1080
1553
|
* survives the process, into the user's own repository, is not read-only under
|
|
1081
1554
|
* any reading, and it now prompts.
|
|
1082
1555
|
*/
|
|
1083
|
-
const PROMPT_EXEMPT_WRITES = new Set(['task_create', 'task_update']);
|
|
1556
|
+
const PROMPT_EXEMPT_WRITES = new Set(['task_create', 'task_update', 'update_goal']);
|
|
1084
1557
|
/**
|
|
1085
1558
|
* Whether a call runs without asking: it declares itself read-only, or it is a
|
|
1086
1559
|
* named exemption above.
|
|
@@ -1190,6 +1663,30 @@ export function toAgentEvent(event, presenter) {
|
|
|
1190
1663
|
};
|
|
1191
1664
|
case 'provider_fallback':
|
|
1192
1665
|
return { kind: 'provider-fallback', text: describeFallback(event) };
|
|
1666
|
+
case 'capability_warning':
|
|
1667
|
+
return {
|
|
1668
|
+
kind: 'capability-warning',
|
|
1669
|
+
capability: event.capability,
|
|
1670
|
+
text: event.message,
|
|
1671
|
+
};
|
|
1672
|
+
case 'message_history_repaired': {
|
|
1673
|
+
const changes = [
|
|
1674
|
+
event.duplicateToolResultsRemoved > 0
|
|
1675
|
+
? `${event.duplicateToolResultsRemoved} duplicate result${event.duplicateToolResultsRemoved === 1 ? '' : 's'} removed`
|
|
1676
|
+
: null,
|
|
1677
|
+
event.orphanedToolResultsRemoved > 0
|
|
1678
|
+
? `${event.orphanedToolResultsRemoved} orphaned result${event.orphanedToolResultsRemoved === 1 ? '' : 's'} removed`
|
|
1679
|
+
: null,
|
|
1680
|
+
event.syntheticToolResultsInserted > 0
|
|
1681
|
+
? `${event.syntheticToolResultsInserted} interrupted call${event.syntheticToolResultsInserted === 1 ? '' : 's'} closed with unknown outcome`
|
|
1682
|
+
: null,
|
|
1683
|
+
].filter((part) => part !== null);
|
|
1684
|
+
return {
|
|
1685
|
+
kind: 'history-repair',
|
|
1686
|
+
source: event.source,
|
|
1687
|
+
text: `Tool history repaired before the model call: ${changes.join('; ')}. Verify external state before retrying non-idempotent tools.`,
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1193
1690
|
case 'task_created':
|
|
1194
1691
|
return { kind: 'task', subject: event.subject, status: event.status };
|
|
1195
1692
|
case 'task_updated':
|
|
@@ -1204,7 +1701,10 @@ export function toAgentEvent(event, presenter) {
|
|
|
1204
1701
|
// timeout, a cancellation and a blocked output guardrail arrive. A
|
|
1205
1702
|
// consumer that reads this as success reports one for a run whose
|
|
1206
1703
|
// answer was refused.
|
|
1207
|
-
return {
|
|
1704
|
+
return {
|
|
1705
|
+
kind: 'done',
|
|
1706
|
+
...(event.stopReason ? { stopReason: event.stopReason } : {}),
|
|
1707
|
+
};
|
|
1208
1708
|
case 'run_failed':
|
|
1209
1709
|
// The classification is now carried on the event rather than
|
|
1210
1710
|
// having been flattened away upstream. Shown because "rate
|
|
@@ -1229,7 +1729,11 @@ export function toAgentEvent(event, presenter) {
|
|
|
1229
1729
|
shed: true,
|
|
1230
1730
|
};
|
|
1231
1731
|
case 'compaction_failed':
|
|
1232
|
-
return {
|
|
1732
|
+
return {
|
|
1733
|
+
kind: 'context',
|
|
1734
|
+
text: describeCompactionFailure(event),
|
|
1735
|
+
shed: false,
|
|
1736
|
+
};
|
|
1233
1737
|
default:
|
|
1234
1738
|
return null;
|
|
1235
1739
|
}
|
|
@@ -1333,8 +1837,14 @@ function asTree(steps) {
|
|
|
1333
1837
|
* the four deleted functions did for every tool.
|
|
1334
1838
|
*/
|
|
1335
1839
|
const GENERIC_PRESENTER = {
|
|
1336
|
-
presentCall: (_name, input) => ({
|
|
1337
|
-
|
|
1840
|
+
presentCall: (_name, input) => ({
|
|
1841
|
+
kind: 'generic',
|
|
1842
|
+
label: genericLabel(input),
|
|
1843
|
+
}),
|
|
1844
|
+
presentResult: (_name, _input, result) => ({
|
|
1845
|
+
kind: 'terminal',
|
|
1846
|
+
output: result.output ?? '',
|
|
1847
|
+
}),
|
|
1338
1848
|
};
|
|
1339
1849
|
/**
|
|
1340
1850
|
* The one presentation function this host keeps.
|
|
@@ -1513,7 +2023,9 @@ function logCapabilities(probes) {
|
|
|
1513
2023
|
const summary = probes
|
|
1514
2024
|
.map((p) => `${p.specifier.split('/').pop()} ${p.state === 'present' ? 'yes' : 'no'}`)
|
|
1515
2025
|
.join(' · ');
|
|
1516
|
-
log.info(summary, {
|
|
2026
|
+
log.info(summary, {
|
|
2027
|
+
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.CAPABILITY_DETECTED,
|
|
2028
|
+
});
|
|
1517
2029
|
for (const probe of probes) {
|
|
1518
2030
|
if (probe.state === 'broken') {
|
|
1519
2031
|
log.error('Capability probe failed to load', {
|
|
@@ -1545,6 +2057,15 @@ function emptySession(errorHint, errorKind = 'environment') {
|
|
|
1545
2057
|
});
|
|
1546
2058
|
return {
|
|
1547
2059
|
hasProvider: false,
|
|
2060
|
+
// A refused session ran nothing, so there is nothing confining anything.
|
|
2061
|
+
// Reporting an enforced control here would be a claim about a sandbox
|
|
2062
|
+
// this path never built.
|
|
2063
|
+
sandbox: { unconfined: true, enforced: [], required: [] },
|
|
2064
|
+
// No provider, so no summary can be built. Refusing is the honest
|
|
2065
|
+
// answer; returning `null` would say "nothing to shed".
|
|
2066
|
+
compact: async () => {
|
|
2067
|
+
throw new Error('No provider: pick one with /model before compacting.');
|
|
2068
|
+
},
|
|
1548
2069
|
errorKind,
|
|
1549
2070
|
providerSummary: null,
|
|
1550
2071
|
modelSummary: null,
|
|
@@ -1563,6 +2084,7 @@ function emptySession(errorHint, errorKind = 'environment') {
|
|
|
1563
2084
|
errorHint,
|
|
1564
2085
|
// No turn can run here, so no prompt can have been answered.
|
|
1565
2086
|
approvalLatched: () => false,
|
|
2087
|
+
resetApprovalLatch: () => { },
|
|
1566
2088
|
// No registry was built, so there is no roster to report on.
|
|
1567
2089
|
promptExemptTools: () => [],
|
|
1568
2090
|
send: async function* () {
|