@namzu/cli 18.0.0 → 19.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5898 -0
- package/README.md +23 -24
- package/dist/bin.js +12 -1
- package/dist/bin.js.map +1 -1
- package/dist/checkpoints/store.d.ts +75 -0
- package/dist/checkpoints/store.d.ts.map +1 -0
- package/dist/checkpoints/store.js +177 -0
- package/dist/checkpoints/store.js.map +1 -0
- package/dist/checkpoints/wrap.d.ts +15 -0
- package/dist/checkpoints/wrap.d.ts.map +1 -0
- package/dist/checkpoints/wrap.js +37 -0
- package/dist/checkpoints/wrap.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +3 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +15 -2
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/provider-wait.d.ts +44 -0
- package/dist/commands/provider-wait.d.ts.map +1 -0
- package/dist/commands/provider-wait.js +65 -0
- package/dist/commands/provider-wait.js.map +1 -0
- package/dist/commands/run-flags.d.ts +13 -1
- package/dist/commands/run-flags.d.ts.map +1 -1
- package/dist/commands/run-flags.js +41 -0
- package/dist/commands/run-flags.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +34 -13
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +133 -78
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/stdin.d.ts +17 -0
- package/dist/commands/stdin.d.ts.map +1 -0
- package/dist/commands/stdin.js +69 -0
- package/dist/commands/stdin.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +122 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +92 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/doctrine.d.ts +22 -0
- package/dist/context/doctrine.d.ts.map +1 -0
- package/dist/context/doctrine.js +22 -0
- package/dist/context/doctrine.js.map +1 -0
- package/dist/context/environment.d.ts +2 -0
- package/dist/context/environment.d.ts.map +1 -1
- package/dist/context/environment.js +3 -0
- package/dist/context/environment.js.map +1 -1
- package/dist/context/turn-snapshot.d.ts +59 -0
- package/dist/context/turn-snapshot.d.ts.map +1 -0
- package/dist/context/turn-snapshot.js +122 -0
- package/dist/context/turn-snapshot.js.map +1 -0
- package/dist/integrations/mcp/servers.d.ts +19 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +23 -2
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/plugins/runtime.d.ts +8 -2
- package/dist/integrations/plugins/runtime.d.ts.map +1 -1
- package/dist/integrations/plugins/runtime.js +24 -12
- package/dist/integrations/plugins/runtime.js.map +1 -1
- package/dist/integrations/providers/chain-capabilities.js +1 -1
- package/dist/integrations/providers/chain-capabilities.js.map +1 -1
- package/dist/integrations/providers/credential-store.d.ts +1 -1
- package/dist/integrations/providers/credential-store.js +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +7 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +2 -2
- package/dist/integrations/providers/registry.js +2 -2
- package/dist/integrations/sessions/store.d.ts +14 -22
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +78 -175
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +6 -2
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/state/identity.d.ts +21 -0
- package/dist/integrations/state/identity.d.ts.map +1 -0
- package/dist/integrations/state/identity.js +49 -0
- package/dist/integrations/state/identity.js.map +1 -0
- package/dist/integrations/state/immutable-json.d.ts +10 -0
- package/dist/integrations/state/immutable-json.d.ts.map +1 -0
- package/dist/integrations/state/immutable-json.js +38 -0
- package/dist/integrations/state/immutable-json.js.map +1 -0
- package/dist/integrations/state/project.d.ts +10 -0
- package/dist/integrations/state/project.d.ts.map +1 -0
- package/dist/integrations/state/project.js +15 -0
- package/dist/integrations/state/project.js.map +1 -0
- package/dist/integrations/state/report.d.ts.map +1 -1
- package/dist/integrations/state/report.js +42 -12
- package/dist/integrations/state/report.js.map +1 -1
- package/dist/integrations/subagents/definitions.d.ts +19 -0
- package/dist/integrations/subagents/definitions.d.ts.map +1 -0
- package/dist/integrations/subagents/definitions.js +26 -0
- package/dist/integrations/subagents/definitions.js.map +1 -0
- package/dist/integrations/subagents/parent.d.ts +13 -0
- package/dist/integrations/subagents/parent.d.ts.map +1 -0
- package/dist/integrations/subagents/parent.js +57 -0
- package/dist/integrations/subagents/parent.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +27 -3
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +302 -82
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/memory/store.d.ts +35 -15
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +79 -25
- package/dist/memory/store.js.map +1 -1
- package/dist/permissions/mode.d.ts +12 -20
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +18 -3
- package/dist/permissions/mode.js.map +1 -1
- package/dist/release-notes.d.ts +20 -0
- package/dist/release-notes.d.ts.map +1 -0
- package/dist/release-notes.js +64 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts.map +1 -1
- package/dist/tui/AgentExplorer.js +14 -18
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +843 -208
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/BrandHeader.d.ts +12 -0
- package/dist/tui/BrandHeader.d.ts.map +1 -0
- package/dist/tui/BrandHeader.js +17 -0
- package/dist/tui/BrandHeader.js.map +1 -0
- package/dist/tui/ChoicePicker.d.ts +11 -1
- package/dist/tui/ChoicePicker.d.ts.map +1 -1
- package/dist/tui/ChoicePicker.js +46 -13
- package/dist/tui/ChoicePicker.js.map +1 -1
- package/dist/tui/Composer.d.ts +9 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +21 -5
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/ComposerFrame.d.ts +10 -0
- package/dist/tui/ComposerFrame.d.ts.map +1 -0
- package/dist/tui/ComposerFrame.js +93 -0
- package/dist/tui/ComposerFrame.js.map +1 -0
- package/dist/tui/EditPromptPicker.d.ts.map +1 -1
- package/dist/tui/EditPromptPicker.js +1 -1
- package/dist/tui/EditPromptPicker.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +12 -5
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +17 -32
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +26 -2
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +118 -11
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +2 -2
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +79 -52
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +5 -2
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/TaskList.d.ts +32 -0
- package/dist/tui/TaskList.d.ts.map +1 -0
- package/dist/tui/TaskList.js +67 -0
- package/dist/tui/TaskList.js.map +1 -0
- package/dist/tui/Transcript.d.ts +3 -3
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +16 -34
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +152 -48
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +622 -256
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/choice-selection.d.ts +17 -0
- package/dist/tui/choice-selection.d.ts.map +1 -0
- package/dist/tui/choice-selection.js +38 -0
- package/dist/tui/choice-selection.js.map +1 -0
- package/dist/tui/exit-summary.d.ts +7 -1
- package/dist/tui/exit-summary.d.ts.map +1 -1
- package/dist/tui/exit-summary.js +12 -3
- package/dist/tui/exit-summary.js.map +1 -1
- package/dist/tui/index.d.ts +3 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -3
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/logo.d.ts +7 -22
- package/dist/tui/logo.d.ts.map +1 -1
- package/dist/tui/logo.js +8 -34
- package/dist/tui/logo.js.map +1 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +21 -6
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/permission-review.d.ts +15 -4
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +199 -10
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/provider-selection.d.ts +4 -0
- package/dist/tui/provider-selection.d.ts.map +1 -0
- package/dist/tui/provider-selection.js +16 -0
- package/dist/tui/provider-selection.js.map +1 -0
- package/dist/tui/run-interruption.d.ts +3 -2
- package/dist/tui/run-interruption.d.ts.map +1 -1
- package/dist/tui/run-interruption.js +2 -1
- package/dist/tui/run-interruption.js.map +1 -1
- package/dist/tui/shell-escape.d.ts +42 -0
- package/dist/tui/shell-escape.d.ts.map +1 -0
- package/dist/tui/shell-escape.js +110 -0
- package/dist/tui/shell-escape.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +77 -31
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +343 -303
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/stream-blocks.d.ts +24 -0
- package/dist/tui/stream-blocks.d.ts.map +1 -1
- package/dist/tui/stream-blocks.js +74 -0
- package/dist/tui/stream-blocks.js.map +1 -1
- package/dist/tui/terminal-choice-text.d.ts +8 -0
- package/dist/tui/terminal-choice-text.d.ts.map +1 -0
- package/dist/tui/terminal-choice-text.js +37 -0
- package/dist/tui/terminal-choice-text.js.map +1 -0
- package/dist/tui/theme.d.ts +3 -4
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +16 -17
- package/dist/tui/theme.js.map +1 -1
- package/dist/tui/types.d.ts +8 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +8 -6
package/dist/tui/agent.js
CHANGED
|
@@ -20,18 +20,24 @@
|
|
|
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, DefaultPathBuilder, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry,
|
|
23
|
+
import { BOOT_EVENT_NAMES, BackgroundJobRegistry, DefaultPathBuilder, DiskCheckpointStore, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, GuardedFetchProvider, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, WebFetchTool, batchNeedsReview, buildAskUserQuestionTool, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createReviewHandler, createToolPresenter, generateProjectId, generateRunId, generateSessionId, generateTenantId, generateTopicId, getBuiltinTools, isReviewExempt, query, resumeRun, webGuidanceContribution, withProviderFallback, } from '@namzu/sdk';
|
|
24
24
|
import { SubprocessComputerUseHost } from '@namzu/computer-use';
|
|
25
25
|
import { realpath, stat } from 'node:fs/promises';
|
|
26
26
|
import { join, parse, resolve } from 'node:path';
|
|
27
|
+
import { FileCheckpointStore } from '../checkpoints/store.js';
|
|
28
|
+
import { CHECKPOINTED_TOOLS, withCheckpoints } from '../checkpoints/wrap.js';
|
|
27
29
|
import { probeCapabilities } from '../context/capabilities.js';
|
|
30
|
+
import { NAMZU_DELEGATION_DOCTRINE, NAMZU_PLAN_MODE_DOCTRINE, NAMZU_WORKING_DOCTRINE, } from '../context/doctrine.js';
|
|
28
31
|
import { composeEnvironmentPrompt, readEnvironmentFacts } from '../context/environment.js';
|
|
29
32
|
import { ProjectInstructionTracker } from '../context/project-tracker.js';
|
|
30
33
|
import { resolveSandbox, sandboxResolvedSeverity, } from '../context/sandbox.js';
|
|
34
|
+
import { composeTurnSnapshot, readTurnSnapshot } from '../context/turn-snapshot.js';
|
|
31
35
|
import { connectMcpServers, } from '../integrations/mcp/servers.js';
|
|
32
36
|
import { createCliPluginRuntime } from '../integrations/plugins/runtime.js';
|
|
33
37
|
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';
|
|
34
38
|
import { ensurePrivateStateDirectory } from '../integrations/state/private-directory.js';
|
|
39
|
+
import { discoverAgentDefinitions } from '../integrations/subagents/definitions.js';
|
|
40
|
+
import { SubagentPathBuilder, resolveSubagentParent } from '../integrations/subagents/parent.js';
|
|
35
41
|
import { CLI_INTERACTIVE_RUN_TIMEOUT_MS } from '../integrations/subagents/policy.js';
|
|
36
42
|
import { createSubagentRuntime } from '../integrations/subagents/runtime.js';
|
|
37
43
|
import { cliLogger } from '../logging.js';
|
|
@@ -238,7 +244,17 @@ async function drainIterator(iterator) {
|
|
|
238
244
|
*/
|
|
239
245
|
export async function probeAgentSession() {
|
|
240
246
|
const read = readPreferences();
|
|
247
|
+
// Bracketed in the log because this is where a boot has stalled without
|
|
248
|
+
// a record on either side: it reads credential files, and on WSL it asks
|
|
249
|
+
// Windows for the paired home. A hang that shows the last line before it
|
|
250
|
+
// and nothing after is this step.
|
|
251
|
+
const discoveryStartedAt = Date.now();
|
|
252
|
+
cliLogger().debug('discovering provider credentials');
|
|
241
253
|
const detected = await discoverProviders();
|
|
254
|
+
cliLogger().debug('provider credentials discovered', {
|
|
255
|
+
'namzu.boot.discovery_ms': Date.now() - discoveryStartedAt,
|
|
256
|
+
'namzu.boot.detected_count': detected.length,
|
|
257
|
+
});
|
|
242
258
|
switch (read.status) {
|
|
243
259
|
case 'ok':
|
|
244
260
|
return {
|
|
@@ -357,9 +373,21 @@ function builtinTools(backgroundJobs) {
|
|
|
357
373
|
return [tool.name === 'bash' ? foregroundOnlyBash(tool) : tool];
|
|
358
374
|
});
|
|
359
375
|
}
|
|
360
|
-
function buildToolRegistry(cwd, projectStateRoot = join(cwd, '.namzu'), backgroundJobs = true) {
|
|
376
|
+
function buildToolRegistry(cwd, projectStateRoot = join(cwd, '.namzu'), backgroundJobs = true, checkpoints) {
|
|
361
377
|
const registry = new ToolRegistry();
|
|
362
378
|
registry.register(builtinTools(backgroundJobs));
|
|
379
|
+
// The file tools take a checkpoint before they write, so `/restore` can
|
|
380
|
+
// put the tree back. Only the session's own registry: a sub-agent's
|
|
381
|
+
// writes are not checkpointed yet, and the page says so.
|
|
382
|
+
if (checkpoints) {
|
|
383
|
+
for (const name of CHECKPOINTED_TOOLS) {
|
|
384
|
+
const tool = registry.get(name);
|
|
385
|
+
if (!tool)
|
|
386
|
+
continue;
|
|
387
|
+
registry.unregister(name);
|
|
388
|
+
registry.register(withCheckpoints(tool, checkpoints));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
363
391
|
// SDK memory: the agent gets search_memory / read_memory / save_memory over
|
|
364
392
|
// a structured store in this Project's generated-state directory. CLI
|
|
365
393
|
// surfaces inject the central application-home hierarchy; embedded callers
|
|
@@ -711,8 +739,46 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
711
739
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.SANDBOX_RESOLVED,
|
|
712
740
|
'namzu.sandbox.unconfined': sandbox.unconfined,
|
|
713
741
|
});
|
|
714
|
-
|
|
715
|
-
|
|
742
|
+
// Always built: the executor hands it to the tools only where it is
|
|
743
|
+
// safe — on the host, or inside a sandbox that can start a detached
|
|
744
|
+
// process — so a session under a sandbox that cannot simply has none.
|
|
745
|
+
const backgroundJobs = true;
|
|
746
|
+
// One registry per session, and jobs bound to the SESSION: a dev server
|
|
747
|
+
// started in one turn is still there in the next, and the kernel tells
|
|
748
|
+
// the model when a job ends. Stopped when the session closes, below.
|
|
749
|
+
// Withheld under a sandbox for the reason the kernel gives: the registry
|
|
750
|
+
// runs on the host and must not sit beside a sandbox in one tool context.
|
|
751
|
+
const jobRegistry = backgroundJobs ? new BackgroundJobRegistry() : undefined;
|
|
752
|
+
const jobOwner = scope.sessionId;
|
|
753
|
+
// Session-scoped and mutable: `/add-dir` adds to it, and every turn reads
|
|
754
|
+
// it fresh — the query, the sandbox binds and the environment prompt.
|
|
755
|
+
const directories = [];
|
|
756
|
+
for (const dir of options.additionalDirectories ?? []) {
|
|
757
|
+
const absolute = resolve(cwd, dir);
|
|
758
|
+
if (absolute !== resolve(cwd) && !directories.includes(absolute))
|
|
759
|
+
directories.push(absolute);
|
|
760
|
+
}
|
|
761
|
+
const sessionDirectories = {
|
|
762
|
+
list: () => [...directories],
|
|
763
|
+
add: async (path) => {
|
|
764
|
+
const absolute = resolve(cwd, path);
|
|
765
|
+
if (absolute === resolve(cwd))
|
|
766
|
+
return {
|
|
767
|
+
added: false,
|
|
768
|
+
path: absolute,
|
|
769
|
+
reason: 'That is the working directory.',
|
|
770
|
+
};
|
|
771
|
+
if (directories.includes(absolute))
|
|
772
|
+
return { added: false, path: absolute, reason: 'Already added.' };
|
|
773
|
+
const entry = await stat(absolute).catch(() => null);
|
|
774
|
+
if (!entry?.isDirectory())
|
|
775
|
+
return { added: false, path: absolute, reason: 'Not a directory.' };
|
|
776
|
+
directories.push(absolute);
|
|
777
|
+
return { added: true, path: absolute };
|
|
778
|
+
},
|
|
779
|
+
};
|
|
780
|
+
const checkpoints = new FileCheckpointStore(join(ensurePrivateStateDirectory(projectStateRoot, 'checkpoints'), scope.sessionId), cwd);
|
|
781
|
+
const { registry, memoryStore } = buildToolRegistry(cwd, projectStateRoot, backgroundJobs, checkpoints);
|
|
716
782
|
// Package presence is not tool reachability. The CLI used to probe and
|
|
717
783
|
// report @namzu/computer-use without ever constructing its host or mounting
|
|
718
784
|
// SDK's computer_use definition, so even an installed, healthy package was
|
|
@@ -732,6 +798,28 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
732
798
|
catch (error) {
|
|
733
799
|
computerUseError = error instanceof Error ? error : new Error(String(error));
|
|
734
800
|
await candidate.dispose().catch(() => { });
|
|
801
|
+
// Mounted anyway, with every capability false and the reason on it.
|
|
802
|
+
// A tool that is absent is a tool the model reasons about from the
|
|
803
|
+
// wrong premise; a tool that says "this desktop did not answer, and
|
|
804
|
+
// why" is one call the model reads once and does not repeat.
|
|
805
|
+
registry.register(createComputerUseTool({
|
|
806
|
+
id: candidate.id,
|
|
807
|
+
capabilities: {
|
|
808
|
+
...candidate.capabilities,
|
|
809
|
+
screenshot: false,
|
|
810
|
+
mouse: false,
|
|
811
|
+
keyboard: false,
|
|
812
|
+
cursorPosition: false,
|
|
813
|
+
clipboard: false,
|
|
814
|
+
unavailableReason: describeError(computerUseError),
|
|
815
|
+
},
|
|
816
|
+
getDisplayGeometry: async () => {
|
|
817
|
+
throw computerUseError;
|
|
818
|
+
},
|
|
819
|
+
execute: async () => {
|
|
820
|
+
throw computerUseError;
|
|
821
|
+
},
|
|
822
|
+
}));
|
|
735
823
|
}
|
|
736
824
|
}
|
|
737
825
|
// Registered only on the main session path. Sub-agents call
|
|
@@ -789,10 +877,19 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
789
877
|
// This session passes a `taskStore` to query() below, which registers the
|
|
790
878
|
// task tools deferred — so `search_tools` has something to find here.
|
|
791
879
|
registry.register([SearchToolsTool]);
|
|
880
|
+
// Web reach, opted into in the config file and nowhere else. The parent's
|
|
881
|
+
// registry only: a child's config carries no provider, and a tool whose
|
|
882
|
+
// provider is missing is a tool that reports itself unwired — truthful,
|
|
883
|
+
// and noise. The guarded provider refuses private and loopback addresses
|
|
884
|
+
// and bounds redirects and body; every fetch is reviewed like a shell
|
|
885
|
+
// command (see `isPromptExempt`).
|
|
886
|
+
const webCapability = options.web?.fetch ? { fetch: new GuardedFetchProvider() } : undefined;
|
|
887
|
+
if (webCapability)
|
|
888
|
+
registry.register(WebFetchTool);
|
|
792
889
|
// Native sub-agents: register the canonical `Agent` tool so the model can
|
|
793
890
|
// delegate a self-contained task to a fresh sub-agent (own context window).
|
|
794
891
|
// Best-effort — if the runtime can't stand up, the chat still works.
|
|
795
|
-
|
|
892
|
+
const delegationScopes = new Map();
|
|
796
893
|
let subagentRuntime;
|
|
797
894
|
// Stays empty when the runtime below throws, which is the honest answer: the
|
|
798
895
|
// catch is non-fatal and the session then genuinely has no delegate to
|
|
@@ -800,10 +897,28 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
800
897
|
// would name agents that are not there.
|
|
801
898
|
let allowedAgentIds = [];
|
|
802
899
|
try {
|
|
900
|
+
// Agents the project or user defined in files. A file that cannot be
|
|
901
|
+
// loaded is named with its reason rather than silently absent: "namzu
|
|
902
|
+
// ignored my reviewer" and "namzu never saw it" call for opposite fixes.
|
|
903
|
+
const discovered = await discoverAgentDefinitions({ cwd });
|
|
904
|
+
for (const skipped of discovered.skipped) {
|
|
905
|
+
cliLogger().warn('agent definition skipped', {
|
|
906
|
+
'namzu.agent.definition.path': skipped.path,
|
|
907
|
+
'namzu.agent.definition.reason': skipped.reason,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
803
910
|
const sub = await createSubagentRuntime({
|
|
804
911
|
cwd,
|
|
805
912
|
model,
|
|
806
|
-
|
|
913
|
+
tokenBudget: options.limits?.tokenBudget,
|
|
914
|
+
definitions: discovered.definitions,
|
|
915
|
+
pathBuilder: new SubagentPathBuilder(projectStateRoot, scope.projectId),
|
|
916
|
+
resolveParent: async (runId) => {
|
|
917
|
+
const parent = delegationScopes.get(runId);
|
|
918
|
+
if (!parent)
|
|
919
|
+
throw new Error(`Run ${runId} no longer owns delegation authority`);
|
|
920
|
+
return resolveSubagentParent(parent, cwd, options.stateRoot ? hierarchyRoot : undefined);
|
|
921
|
+
},
|
|
807
922
|
sandboxWorkspace,
|
|
808
923
|
resolveResumeHandler: (runId) => delegatedResumeHandlers.get(runId),
|
|
809
924
|
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
@@ -850,7 +965,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
850
965
|
});
|
|
851
966
|
subagentRuntime = sub;
|
|
852
967
|
registry.register([sub.agentTool]);
|
|
853
|
-
subagentGateway = sub.gateway;
|
|
854
968
|
allowedAgentIds = sub.allowedAgentIds;
|
|
855
969
|
}
|
|
856
970
|
catch (err) {
|
|
@@ -864,24 +978,90 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
864
978
|
// and got none had nothing on stderr to say why.
|
|
865
979
|
cliLogger().warn('sub-agent runtime unavailable this session', exceptionAttributes(err));
|
|
866
980
|
}
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
// a
|
|
981
|
+
// `ask_user_question`, where somebody can answer. The SDK tool parks the
|
|
982
|
+
// run through the handler it was BUILT with, so that handler reads the
|
|
983
|
+
// turn's answerer through a holder the prelude fills: the tool is per
|
|
984
|
+
// session, the person answering is per turn. Needs the delegation
|
|
985
|
+
// gateway the tool builder requires; a session without one has no
|
|
986
|
+
// question tool either, and says nothing — it also has no `Agent`.
|
|
987
|
+
let currentOnQuestion;
|
|
988
|
+
if (options.askUser && subagentRuntime) {
|
|
989
|
+
const parkQuestion = async (request) => {
|
|
990
|
+
if (request.type !== 'user_question')
|
|
991
|
+
return { action: 'continue' };
|
|
992
|
+
const ask = currentOnQuestion;
|
|
993
|
+
if (!ask)
|
|
994
|
+
return { action: 'continue' };
|
|
995
|
+
const answer = await ask(request.question);
|
|
996
|
+
switch (answer.kind) {
|
|
997
|
+
case 'answer':
|
|
998
|
+
return {
|
|
999
|
+
action: 'answer_question',
|
|
1000
|
+
selectedOptionIds: [...answer.selectedOptionIds],
|
|
1001
|
+
...(answer.freeText !== undefined ? { freeText: answer.freeText } : {}),
|
|
1002
|
+
questionId: request.question.questionId,
|
|
1003
|
+
};
|
|
1004
|
+
case 'abort':
|
|
1005
|
+
return { action: 'abort', reason: 'The user declined to answer.' };
|
|
1006
|
+
default:
|
|
1007
|
+
return { action: 'continue' };
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
// The park request carries the run id of the call that asked; the
|
|
1011
|
+
// handler above routes by the question, not by the run, and no durable
|
|
1012
|
+
// park recorder is supplied.
|
|
1013
|
+
registry.register(buildAskUserQuestionTool({ resumeHandler: parkQuestion }));
|
|
1014
|
+
}
|
|
1015
|
+
// Task store → query registers task_create / task_update / task_list and
|
|
1016
|
+
// emits task_created/task_updated, so the agent can track a plan for the
|
|
1017
|
+
// current request. Tasks are run-scoped. The kernel's default availability
|
|
1018
|
+
// for them is `deferred`; this session overrides that to `active` at the
|
|
1019
|
+
// query call, because the doctrine tells the model to plan with them and a
|
|
1020
|
+
// tool it must search for first is a tool it skips.
|
|
870
1021
|
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
//
|
|
1022
|
+
// `search_tools` stays mounted above even so: a tool server or plugin can
|
|
1023
|
+
// still register a deferred roster, and that is what the search is for. The
|
|
1024
|
+
// task tools are registered inside query(), after this function returns,
|
|
1025
|
+
// which is why the connect line reports no count of them — counting here
|
|
1026
|
+
// would mean restating query's registration order in the CLI.
|
|
875
1027
|
//
|
|
876
1028
|
// It is also why `toolNames` below reads the registry rather than a list
|
|
877
1029
|
// captured on this line. The count at connect time is unchanged; what
|
|
878
1030
|
// changes is that asking again later gets a later answer.
|
|
879
1031
|
ensurePrivateStateDirectory(projectStateRoot, 'tenants');
|
|
880
|
-
const
|
|
1032
|
+
const taskStoreForRun = (runId, tenantId) => new DiskTaskStore({
|
|
881
1033
|
baseDir: projectStateRoot,
|
|
882
|
-
defaultRunId:
|
|
883
|
-
tenantId
|
|
1034
|
+
defaultRunId: runId,
|
|
1035
|
+
tenantId,
|
|
884
1036
|
});
|
|
1037
|
+
let selectedTaskStore;
|
|
1038
|
+
let taskSelectionGeneration = 0;
|
|
1039
|
+
const resetTaskStore = () => {
|
|
1040
|
+
selectedTaskStore = undefined;
|
|
1041
|
+
taskSelectionGeneration += 1;
|
|
1042
|
+
};
|
|
1043
|
+
const matchesCurrentScope = (candidate) => candidate.sessionId === scope.sessionId &&
|
|
1044
|
+
candidate.projectId === scope.projectId &&
|
|
1045
|
+
candidate.tenantId === scope.tenantId &&
|
|
1046
|
+
candidate.topicId === scope.topicId;
|
|
1047
|
+
const currentTaskStore = () => {
|
|
1048
|
+
if (selectedTaskStore && !matchesCurrentScope(selectedTaskStore.scope))
|
|
1049
|
+
resetTaskStore();
|
|
1050
|
+
return selectedTaskStore?.store;
|
|
1051
|
+
};
|
|
1052
|
+
const beginTaskStoreReadout = () => {
|
|
1053
|
+
// A starting turn must not show its predecessor's plan while credentials
|
|
1054
|
+
// and other asynchronous setup are still being prepared.
|
|
1055
|
+
resetTaskStore();
|
|
1056
|
+
const generation = taskSelectionGeneration;
|
|
1057
|
+
return (runId, runScope) => {
|
|
1058
|
+
const store = taskStoreForRun(runId, runScope.tenantId);
|
|
1059
|
+
if (generation === taskSelectionGeneration && matchesCurrentScope(runScope)) {
|
|
1060
|
+
selectedTaskStore = { scope: { ...runScope }, store };
|
|
1061
|
+
}
|
|
1062
|
+
return store;
|
|
1063
|
+
};
|
|
1064
|
+
};
|
|
885
1065
|
// Persists across turns: once the user picks "approve all", later tool
|
|
886
1066
|
// batches in this session run without prompting.
|
|
887
1067
|
const approval = { all: false };
|
|
@@ -903,12 +1083,31 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
903
1083
|
// registries, so executable plugins remain a top-level-session capability.
|
|
904
1084
|
let pluginRuntime;
|
|
905
1085
|
try {
|
|
906
|
-
pluginRuntime = await createCliPluginRuntime(options.plugins, registry, cwd);
|
|
1086
|
+
pluginRuntime = await createCliPluginRuntime(options.plugins, registry, cwd, options.hooks);
|
|
907
1087
|
}
|
|
908
1088
|
catch (error) {
|
|
909
1089
|
await Promise.allSettled([mcp.close(), computerUseHost?.dispose()]);
|
|
910
1090
|
return emptySession(describeError(error));
|
|
911
1091
|
}
|
|
1092
|
+
// The session's own lifecycle, for hooks that set up or tear down
|
|
1093
|
+
// something per session rather than per run. The run id is minted for
|
|
1094
|
+
// these two calls: they belong to no turn. `session_start` waits for
|
|
1095
|
+
// the first turn rather than firing here, because the conversation id
|
|
1096
|
+
// the scope holds at construction is provisional — it is replaced when
|
|
1097
|
+
// the conversation is first made durable — and a hook given the
|
|
1098
|
+
// provisional id could never match it to a run.
|
|
1099
|
+
const sessionPlugins = pluginRuntime;
|
|
1100
|
+
const sessionHookRunId = generateRunId();
|
|
1101
|
+
let sessionStarted = false;
|
|
1102
|
+
const announceSessionStart = async () => {
|
|
1103
|
+
if (!sessionPlugins || sessionStarted)
|
|
1104
|
+
return;
|
|
1105
|
+
sessionStarted = true;
|
|
1106
|
+
await sessionPlugins.manager.executeHooks('session_start', {
|
|
1107
|
+
runId: sessionHookRunId,
|
|
1108
|
+
sessionId: scope.sessionId,
|
|
1109
|
+
});
|
|
1110
|
+
};
|
|
912
1111
|
if (pluginRuntime) {
|
|
913
1112
|
cliLogger().info('discovery complete', {
|
|
914
1113
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.DISCOVERY_COMPLETED,
|
|
@@ -930,9 +1129,19 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
930
1129
|
const operations = new SessionOperationOwner(async () => {
|
|
931
1130
|
const results = await Promise.allSettled([
|
|
932
1131
|
subagentRuntime?.close?.(),
|
|
933
|
-
|
|
1132
|
+
sessionPlugins
|
|
1133
|
+
? sessionPlugins.manager
|
|
1134
|
+
.executeHooks('session_end', {
|
|
1135
|
+
runId: sessionHookRunId,
|
|
1136
|
+
sessionId: scope.sessionId,
|
|
1137
|
+
})
|
|
1138
|
+
.catch(() => [])
|
|
1139
|
+
.then(() => sessionPlugins.close())
|
|
1140
|
+
: undefined,
|
|
934
1141
|
mcp.close(),
|
|
935
1142
|
computerUseHost?.dispose(),
|
|
1143
|
+
jobRegistry?.killOwner(jobOwner),
|
|
1144
|
+
checkpoints.close(),
|
|
936
1145
|
]);
|
|
937
1146
|
const failures = results
|
|
938
1147
|
.filter((result) => result.status === 'rejected')
|
|
@@ -972,6 +1181,200 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
972
1181
|
reasoningEffortDefault = undefined;
|
|
973
1182
|
effortNotice = `Reasoning effort levels could not be established for this session: ${describeError(error)}`;
|
|
974
1183
|
}
|
|
1184
|
+
/**
|
|
1185
|
+
* The kernel's resume with this session's half of the run attached: the
|
|
1186
|
+
* provider, the tools, the working directory, the doctrine — the part a
|
|
1187
|
+
* checkpoint cannot carry. `resumeDurable` and `resumePaused` differ only
|
|
1188
|
+
* in where the run and its store come from.
|
|
1189
|
+
*/
|
|
1190
|
+
const kernelResume = ({ entry, checkpointStore, claimFence, signal, checkpointId, listener, }) => operations.promise(signal, async (ownedSignal) => {
|
|
1191
|
+
const selectTaskStore = beginTaskStoreReadout();
|
|
1192
|
+
// The same prelude a turn runs, and for the same reasons: a lapsed
|
|
1193
|
+
// OAuth token has to be renewed before the provider is used, and the
|
|
1194
|
+
// fallback chain has to be built AFTER that so its members do not
|
|
1195
|
+
// hold a client the refresh just replaced.
|
|
1196
|
+
await prepareProviderCredential(ownedSignal);
|
|
1197
|
+
const pluginSkills = pluginRuntime
|
|
1198
|
+
? await currentPluginSkills(pluginRuntime.skills)
|
|
1199
|
+
: undefined;
|
|
1200
|
+
const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
|
|
1201
|
+
const environmentPrompt = composeEnvironmentPrompt({
|
|
1202
|
+
...(await readEnvironmentFacts(cwd)),
|
|
1203
|
+
additionalDirectories: [...directories],
|
|
1204
|
+
});
|
|
1205
|
+
const systemPrompt = [
|
|
1206
|
+
NAMZU_IDENTITY,
|
|
1207
|
+
NAMZU_WORKING_DOCTRINE,
|
|
1208
|
+
NAMZU_DELEGATION_DOCTRINE,
|
|
1209
|
+
environmentPrompt,
|
|
1210
|
+
memoryPrompt,
|
|
1211
|
+
]
|
|
1212
|
+
.filter((s) => Boolean(s))
|
|
1213
|
+
.join('\n\n') || undefined;
|
|
1214
|
+
const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
1215
|
+
if (delegatedResumeHandlers.has(entry.runId)) {
|
|
1216
|
+
throw new Error(`Run ${entry.runId} already owns a delegated review channel.`);
|
|
1217
|
+
}
|
|
1218
|
+
delegatedResumeHandlers.set(entry.runId, resumeHandler);
|
|
1219
|
+
delegationScopes.set(entry.runId, { ...entry, topicId: scope.topicId });
|
|
1220
|
+
try {
|
|
1221
|
+
return await resumeRun({
|
|
1222
|
+
provider,
|
|
1223
|
+
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1224
|
+
tools: registry,
|
|
1225
|
+
pluginManager: pluginRuntime?.manager,
|
|
1226
|
+
skillRegistry: pluginRuntime?.skills,
|
|
1227
|
+
skills: pluginSkills,
|
|
1228
|
+
taskStore: selectTaskStore(entry.runId, { ...entry, topicId: scope.topicId }),
|
|
1229
|
+
// The same availability the original run registered under.
|
|
1230
|
+
// A resumed run re-registers the task tools; leaving them at
|
|
1231
|
+
// the kernel's `deferred` default would hand the model a plan
|
|
1232
|
+
// it started with active tools and can no longer update.
|
|
1233
|
+
runtimeToolOverrides: {
|
|
1234
|
+
task_create: 'active',
|
|
1235
|
+
task_update: 'active',
|
|
1236
|
+
task_list: 'active',
|
|
1237
|
+
},
|
|
1238
|
+
...(subagentRuntime
|
|
1239
|
+
? {
|
|
1240
|
+
taskScheduler: await subagentRuntime.gatewayForRun(entry.runId),
|
|
1241
|
+
}
|
|
1242
|
+
: {}),
|
|
1243
|
+
authorizationGate: gateFor(options.rules),
|
|
1244
|
+
compactionConfig: compactionConfigFor(options.compaction),
|
|
1245
|
+
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1246
|
+
pathBuilder,
|
|
1247
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1248
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1249
|
+
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
1250
|
+
: {}),
|
|
1251
|
+
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
1252
|
+
// whose `attach` detaches whoever held it before, so a caller
|
|
1253
|
+
// resuming several runs in one process would leave only the last
|
|
1254
|
+
// one covered — and would look covered. A turn owns its process
|
|
1255
|
+
// end to end; a drainer does not.
|
|
1256
|
+
runConfig: {
|
|
1257
|
+
model,
|
|
1258
|
+
...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1259
|
+
timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
1260
|
+
tokenBudget: options.limits?.tokenBudget ?? 1_000_000,
|
|
1261
|
+
maxIterations: options.limits?.maxIterations ?? 50,
|
|
1262
|
+
maxResponseTokens: 8192,
|
|
1263
|
+
permissionMode: 'auto',
|
|
1264
|
+
},
|
|
1265
|
+
agentId: 'namzu',
|
|
1266
|
+
agentName: 'namzu',
|
|
1267
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
1268
|
+
workingDirectory: cwd,
|
|
1269
|
+
...(directories.length > 0 ? { additionalDirectories: [...directories] } : {}),
|
|
1270
|
+
...(options.limits ? { limits: options.limits } : {}),
|
|
1271
|
+
// No `onPermission`: there is nobody at a drainer's terminal, so a
|
|
1272
|
+
// prompt would block the pass forever on a run nobody is watching.
|
|
1273
|
+
// The gate's deny rules still apply.
|
|
1274
|
+
// One presenter for the whole stream, built from the registry this
|
|
1275
|
+
// scope already holds. It was the absence of the registry HERE that
|
|
1276
|
+
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
1277
|
+
// over a `RunEvent` and could not ask a tool anything.
|
|
1278
|
+
resumeHandler,
|
|
1279
|
+
signal: ownedSignal,
|
|
1280
|
+
// Attribution comes from the ENTRY, not from this session: the run
|
|
1281
|
+
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
1282
|
+
// it would file another tenant's work under this one.
|
|
1283
|
+
tenantId: entry.tenantId,
|
|
1284
|
+
projectId: entry.projectId,
|
|
1285
|
+
sessionId: entry.sessionId,
|
|
1286
|
+
// …except the topic, which no checkpoint records — see
|
|
1287
|
+
// `RunStateScope`. This one is the drainer's, and honestly so:
|
|
1288
|
+
// supplied here rather than pretended to have been recovered.
|
|
1289
|
+
topicId: scope.topicId,
|
|
1290
|
+
scope: { ...entry, topicId: scope.topicId },
|
|
1291
|
+
checkpointStore,
|
|
1292
|
+
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1293
|
+
...(checkpointId !== undefined ? { checkpointId } : {}),
|
|
1294
|
+
...(listener ? { listener } : {}),
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
finally {
|
|
1298
|
+
if (delegatedResumeHandlers.get(entry.runId) === resumeHandler) {
|
|
1299
|
+
delegatedResumeHandlers.delete(entry.runId);
|
|
1300
|
+
delegationScopes.delete(entry.runId);
|
|
1301
|
+
await subagentRuntime?.releaseRun(entry.runId);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
/**
|
|
1306
|
+
* `resumeRun` drains the loop and returns a settled run; the events go to a
|
|
1307
|
+
* listener. A small queue turns that into the stream `send` gives, so a
|
|
1308
|
+
* headless caller renders a resumed run exactly as it rendered the turn.
|
|
1309
|
+
*/
|
|
1310
|
+
const resumePausedStream = ({ runId, checkpointId, signal, }) => {
|
|
1311
|
+
const queue = [];
|
|
1312
|
+
let wake;
|
|
1313
|
+
let settled = false;
|
|
1314
|
+
let failure;
|
|
1315
|
+
const presenter = createToolPresenter(registry);
|
|
1316
|
+
// The store the turn's run manager wrote to, built the same way it
|
|
1317
|
+
// built it (see the kernel's `RunPersistence`): the session directory's
|
|
1318
|
+
// `runs/`, attributed to this tenant and project.
|
|
1319
|
+
const store = new DiskCheckpointStore({
|
|
1320
|
+
baseDir: join(pathBuilder.sessionDir(scope.projectId, scope.sessionId), 'runs'),
|
|
1321
|
+
}, {
|
|
1322
|
+
tenantId: scope.tenantId,
|
|
1323
|
+
projectId: scope.projectId,
|
|
1324
|
+
sessionId: scope.sessionId,
|
|
1325
|
+
});
|
|
1326
|
+
const entry = {
|
|
1327
|
+
tenantId: scope.tenantId,
|
|
1328
|
+
projectId: scope.projectId,
|
|
1329
|
+
sessionId: scope.sessionId,
|
|
1330
|
+
runId: runId,
|
|
1331
|
+
};
|
|
1332
|
+
const outcome = kernelResume({
|
|
1333
|
+
entry,
|
|
1334
|
+
checkpointStore: store,
|
|
1335
|
+
...(signal ? { signal } : {}),
|
|
1336
|
+
checkpointId: checkpointId,
|
|
1337
|
+
listener: (event) => {
|
|
1338
|
+
queue.push(event);
|
|
1339
|
+
wake?.();
|
|
1340
|
+
},
|
|
1341
|
+
})
|
|
1342
|
+
.then((result) => {
|
|
1343
|
+
if (!result.resumed) {
|
|
1344
|
+
failure = new Error(result.reason === 'no-checkpoint'
|
|
1345
|
+
? `no checkpoint ${checkpointId} is recorded for run ${runId}`
|
|
1346
|
+
: `run ${runId} is parked on a decision only a person can answer`);
|
|
1347
|
+
}
|
|
1348
|
+
})
|
|
1349
|
+
.catch((err) => {
|
|
1350
|
+
failure = err instanceof Error ? err : new Error(String(err));
|
|
1351
|
+
})
|
|
1352
|
+
.finally(() => {
|
|
1353
|
+
settled = true;
|
|
1354
|
+
wake?.();
|
|
1355
|
+
});
|
|
1356
|
+
return (async function* () {
|
|
1357
|
+
for (;;) {
|
|
1358
|
+
while (queue.length > 0) {
|
|
1359
|
+
const next = queue.shift();
|
|
1360
|
+
if (!next)
|
|
1361
|
+
break;
|
|
1362
|
+
const mapped = toAgentEvent(next, presenter);
|
|
1363
|
+
if (mapped)
|
|
1364
|
+
yield mapped;
|
|
1365
|
+
}
|
|
1366
|
+
if (settled)
|
|
1367
|
+
break;
|
|
1368
|
+
await new Promise((resolve) => {
|
|
1369
|
+
wake = resolve;
|
|
1370
|
+
});
|
|
1371
|
+
wake = undefined;
|
|
1372
|
+
}
|
|
1373
|
+
await outcome;
|
|
1374
|
+
if (failure)
|
|
1375
|
+
yield { kind: 'error', message: failure.message };
|
|
1376
|
+
})();
|
|
1377
|
+
};
|
|
975
1378
|
return {
|
|
976
1379
|
hasProvider: true,
|
|
977
1380
|
sandbox: {
|
|
@@ -989,7 +1392,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
989
1392
|
await prepareProviderCredential(signal);
|
|
990
1393
|
return compactNow({
|
|
991
1394
|
messages,
|
|
992
|
-
config:
|
|
1395
|
+
config: compactionConfigFor(options.compaction),
|
|
993
1396
|
provider,
|
|
994
1397
|
model,
|
|
995
1398
|
signal,
|
|
@@ -1003,6 +1406,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1003
1406
|
.map((t) => t.name)
|
|
1004
1407
|
.filter((name) => !goalToolNames.has(name)),
|
|
1005
1408
|
agentIds: allowedAgentIds,
|
|
1409
|
+
currentTaskStore,
|
|
1410
|
+
resetTaskStore,
|
|
1411
|
+
jobs: () => jobRegistry?.list(jobOwner) ?? [],
|
|
1412
|
+
...(options.hooks ? { hooks: options.hooks } : {}),
|
|
1413
|
+
checkpoints,
|
|
1414
|
+
directories: sessionDirectories,
|
|
1415
|
+
onJobExit: (listener) => jobRegistry?.onExit((job) => {
|
|
1416
|
+
if (job.owner === jobOwner)
|
|
1417
|
+
listener(job);
|
|
1418
|
+
}) ?? (() => { }),
|
|
1006
1419
|
...(subagentRuntime ? { subagents: subagentRuntime.activity } : {}),
|
|
1007
1420
|
get instructionFiles() {
|
|
1008
1421
|
return projectInstructions.instructionFiles;
|
|
@@ -1036,6 +1449,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1036
1449
|
},
|
|
1037
1450
|
promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
|
|
1038
1451
|
send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
|
|
1452
|
+
const selectTaskStore = beginTaskStoreReadout();
|
|
1039
1453
|
const runId = opts?.runId ?? generateRunId();
|
|
1040
1454
|
const turnOpts = { ...opts, runId, signal };
|
|
1041
1455
|
const resumeHandler = makeResumeHandler(approval, opts?.onPermission, opts?.permissionMode ?? options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
@@ -1043,6 +1457,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1043
1457
|
throw new Error(`Run ${runId} already owns a delegated review channel.`);
|
|
1044
1458
|
}
|
|
1045
1459
|
delegatedResumeHandlers.set(runId, resumeHandler);
|
|
1460
|
+
const turnScope = { ...scope };
|
|
1461
|
+
delegationScopes.set(runId, turnScope);
|
|
1046
1462
|
try {
|
|
1047
1463
|
// Renew a lapsed OAuth token before the turn runs (no-op for valid
|
|
1048
1464
|
// tokens and non-subscription credentials).
|
|
@@ -1061,11 +1477,52 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1061
1477
|
const pluginSkills = pluginRuntime
|
|
1062
1478
|
? await currentPluginSkills(pluginRuntime.skills)
|
|
1063
1479
|
: undefined;
|
|
1064
|
-
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
1065
|
-
|
|
1066
|
-
const
|
|
1480
|
+
const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
|
|
1481
|
+
currentOnQuestion = opts?.onQuestion;
|
|
1482
|
+
const [environmentFacts, turnSnapshot] = await Promise.all([
|
|
1483
|
+
readEnvironmentFacts(cwd),
|
|
1484
|
+
readTurnSnapshot(cwd),
|
|
1485
|
+
]);
|
|
1486
|
+
const environmentPrompt = composeEnvironmentPrompt({
|
|
1487
|
+
...environmentFacts,
|
|
1488
|
+
additionalDirectories: [...directories],
|
|
1489
|
+
});
|
|
1490
|
+
// The repository as it stood when THIS turn began, through the
|
|
1491
|
+
// SDK's `turn` placement — the ephemeral trailing message that is
|
|
1492
|
+
// never cached and never enters history. FIRST iteration only:
|
|
1493
|
+
// later iterations work from state the model itself changed, and
|
|
1494
|
+
// `git status` is the honest source for that. A registry per
|
|
1495
|
+
// turn, closed over this turn's snapshot, rather than one
|
|
1496
|
+
// session-scoped holder every send overwrites: two overlapping
|
|
1497
|
+
// sends would otherwise both render whichever ran second.
|
|
1498
|
+
const turnSnapshotPrompt = turnSnapshot ? composeTurnSnapshot(turnSnapshot) : null;
|
|
1499
|
+
const promptContributions = new PromptContributionRegistry();
|
|
1500
|
+
promptContributions.register({
|
|
1501
|
+
id: 'namzu.turn-snapshot',
|
|
1502
|
+
placement: 'turn',
|
|
1503
|
+
render: ({ iteration }) => (iteration === 1 ? turnSnapshotPrompt : null),
|
|
1504
|
+
});
|
|
1505
|
+
// The citation rules that come with the web tools, only when the
|
|
1506
|
+
// tools are there: guidance about a capability the turn does not
|
|
1507
|
+
// have reads as a capability it should be looking for.
|
|
1508
|
+
if (webCapability)
|
|
1509
|
+
promptContributions.register(webGuidanceContribution);
|
|
1510
|
+
const systemPrompt = [
|
|
1511
|
+
NAMZU_IDENTITY,
|
|
1512
|
+
NAMZU_WORKING_DOCTRINE,
|
|
1513
|
+
NAMZU_DELEGATION_DOCTRINE,
|
|
1514
|
+
// Present only while the turn runs under `plan`. A mode change
|
|
1515
|
+
// is rare, so the cached prefix it re-keys is a price paid once
|
|
1516
|
+
// per switch rather than once per turn.
|
|
1517
|
+
opts?.permissionMode === 'plan' ? NAMZU_PLAN_MODE_DOCTRINE : undefined,
|
|
1518
|
+
environmentPrompt,
|
|
1519
|
+
memoryPrompt,
|
|
1520
|
+
opts?.extraSystem,
|
|
1521
|
+
]
|
|
1067
1522
|
.filter((s) => Boolean(s))
|
|
1068
1523
|
.join('\n\n') || undefined;
|
|
1524
|
+
await announceSessionStart();
|
|
1525
|
+
checkpoints.beginTurn(lastUserText(messages));
|
|
1069
1526
|
let capturedAuthority;
|
|
1070
1527
|
if (opts?.goalRound) {
|
|
1071
1528
|
if (!opts.runId)
|
|
@@ -1095,6 +1552,14 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1095
1552
|
try {
|
|
1096
1553
|
yield* runTurn({
|
|
1097
1554
|
provider,
|
|
1555
|
+
compactionConfig: compactionConfigFor(options.compaction),
|
|
1556
|
+
...(options.compaction?.consolidate ? { consolidateInto: memoryStore } : {}),
|
|
1557
|
+
...(jobRegistry
|
|
1558
|
+
? {
|
|
1559
|
+
backgroundJobs: jobRegistry,
|
|
1560
|
+
backgroundJobOwner: jobOwner,
|
|
1561
|
+
}
|
|
1562
|
+
: {}),
|
|
1098
1563
|
// Constructed HERE, per turn, and that is not an optimisation to
|
|
1099
1564
|
// undo. `refreshTokenIfNeeded` above replaces the head's client
|
|
1100
1565
|
// object when an OAuth token rotates, so a member list built once at
|
|
@@ -1108,21 +1573,33 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1108
1573
|
pluginManager: pluginRuntime?.manager,
|
|
1109
1574
|
skillRegistry: pluginRuntime?.skills,
|
|
1110
1575
|
skills: pluginSkills,
|
|
1111
|
-
scope,
|
|
1576
|
+
scope: turnScope,
|
|
1112
1577
|
pathBuilder,
|
|
1113
1578
|
workingDirectory: cwd,
|
|
1579
|
+
...(directories.length > 0 ? { additionalDirectories: [...directories] } : {}),
|
|
1580
|
+
...(options.limits ? { limits: options.limits } : {}),
|
|
1114
1581
|
sandboxWorkspace,
|
|
1115
1582
|
rules: options.rules,
|
|
1116
1583
|
reviewAnswer: options.reviewAnswer,
|
|
1117
1584
|
maxAnswerReviews: options.maxAnswerReviews,
|
|
1118
1585
|
promoteMemory,
|
|
1119
|
-
taskStore,
|
|
1586
|
+
taskStore: selectTaskStore(runId, turnScope),
|
|
1120
1587
|
systemPrompt,
|
|
1121
1588
|
messages,
|
|
1122
1589
|
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1123
1590
|
opts: turnOpts,
|
|
1124
1591
|
resumeHandler,
|
|
1125
|
-
taskGateway:
|
|
1592
|
+
taskGateway: await subagentRuntime?.gatewayForRun(runId),
|
|
1593
|
+
promptContributions,
|
|
1594
|
+
...(webCapability ? { web: webCapability } : {}),
|
|
1595
|
+
// Active, not deferred: the doctrine tells the model to open a
|
|
1596
|
+
// task list for multi-step work, and a tool it has to search
|
|
1597
|
+
// for first is a tool it will skip.
|
|
1598
|
+
runtimeToolOverrides: {
|
|
1599
|
+
task_create: 'active',
|
|
1600
|
+
task_update: 'active',
|
|
1601
|
+
task_list: 'active',
|
|
1602
|
+
},
|
|
1126
1603
|
onRunEvent: options.onRunEvent,
|
|
1127
1604
|
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1128
1605
|
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
@@ -1143,94 +1620,18 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1143
1620
|
finally {
|
|
1144
1621
|
if (delegatedResumeHandlers.get(runId) === resumeHandler) {
|
|
1145
1622
|
delegatedResumeHandlers.delete(runId);
|
|
1623
|
+
delegationScopes.delete(runId);
|
|
1624
|
+
await subagentRuntime?.releaseRun(runId);
|
|
1146
1625
|
}
|
|
1147
1626
|
}
|
|
1148
1627
|
})()),
|
|
1149
|
-
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) =>
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
await prepareProviderCredential(ownedSignal);
|
|
1155
|
-
const pluginSkills = pluginRuntime
|
|
1156
|
-
? await currentPluginSkills(pluginRuntime.skills)
|
|
1157
|
-
: undefined;
|
|
1158
|
-
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
1159
|
-
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
1160
|
-
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
|
|
1161
|
-
.filter((s) => Boolean(s))
|
|
1162
|
-
.join('\n\n') || undefined;
|
|
1163
|
-
const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
1164
|
-
if (delegatedResumeHandlers.has(entry.runId)) {
|
|
1165
|
-
throw new Error(`Run ${entry.runId} already owns a delegated review channel.`);
|
|
1166
|
-
}
|
|
1167
|
-
delegatedResumeHandlers.set(entry.runId, resumeHandler);
|
|
1168
|
-
try {
|
|
1169
|
-
return await resumeRun({
|
|
1170
|
-
provider,
|
|
1171
|
-
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1172
|
-
tools: registry,
|
|
1173
|
-
pluginManager: pluginRuntime?.manager,
|
|
1174
|
-
skillRegistry: pluginRuntime?.skills,
|
|
1175
|
-
skills: pluginSkills,
|
|
1176
|
-
taskStore,
|
|
1177
|
-
...(subagentGateway ? { taskGateway: subagentGateway } : {}),
|
|
1178
|
-
authorizationGate: gateFor(options.rules),
|
|
1179
|
-
compactionConfig: COMPACTION_CONFIG,
|
|
1180
|
-
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1181
|
-
pathBuilder,
|
|
1182
|
-
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1183
|
-
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1184
|
-
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
1185
|
-
: {}),
|
|
1186
|
-
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
1187
|
-
// whose `attach` detaches whoever held it before, so a caller
|
|
1188
|
-
// resuming several runs in one process would leave only the last
|
|
1189
|
-
// one covered — and would look covered. A turn owns its process
|
|
1190
|
-
// end to end; a drainer does not.
|
|
1191
|
-
runConfig: {
|
|
1192
|
-
model,
|
|
1193
|
-
...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1194
|
-
timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
1195
|
-
tokenBudget: 1_000_000,
|
|
1196
|
-
maxIterations: 50,
|
|
1197
|
-
maxResponseTokens: 8192,
|
|
1198
|
-
permissionMode: 'auto',
|
|
1199
|
-
},
|
|
1200
|
-
agentId: 'namzu',
|
|
1201
|
-
agentName: 'namzu',
|
|
1202
|
-
...(systemPrompt ? { systemPrompt } : {}),
|
|
1203
|
-
workingDirectory: cwd,
|
|
1204
|
-
// No `onPermission`: there is nobody at a drainer's terminal, so a
|
|
1205
|
-
// prompt would block the pass forever on a run nobody is watching.
|
|
1206
|
-
// The gate's deny rules still apply.
|
|
1207
|
-
// One presenter for the whole stream, built from the registry this
|
|
1208
|
-
// scope already holds. It was the absence of the registry HERE that
|
|
1209
|
-
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
1210
|
-
// over a `RunEvent` and could not ask a tool anything.
|
|
1211
|
-
resumeHandler,
|
|
1212
|
-
signal: ownedSignal,
|
|
1213
|
-
// Attribution comes from the ENTRY, not from this session: the run
|
|
1214
|
-
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
1215
|
-
// it would file another tenant's work under this one.
|
|
1216
|
-
tenantId: entry.tenantId,
|
|
1217
|
-
projectId: entry.projectId,
|
|
1218
|
-
sessionId: entry.sessionId,
|
|
1219
|
-
// …except the topic, which no checkpoint records — see
|
|
1220
|
-
// `RunStateScope`. This one is the drainer's, and honestly so:
|
|
1221
|
-
// supplied here rather than pretended to have been recovered.
|
|
1222
|
-
topicId: scope.topicId,
|
|
1223
|
-
scope: { ...entry, topicId: scope.topicId },
|
|
1224
|
-
checkpointStore,
|
|
1225
|
-
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1226
|
-
});
|
|
1227
|
-
}
|
|
1228
|
-
finally {
|
|
1229
|
-
if (delegatedResumeHandlers.get(entry.runId) === resumeHandler) {
|
|
1230
|
-
delegatedResumeHandlers.delete(entry.runId);
|
|
1231
|
-
}
|
|
1232
|
-
}
|
|
1628
|
+
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => kernelResume({
|
|
1629
|
+
entry,
|
|
1630
|
+
checkpointStore,
|
|
1631
|
+
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1632
|
+
...(signal ? { signal } : {}),
|
|
1233
1633
|
}),
|
|
1634
|
+
resumePaused: (params) => resumePausedStream(params),
|
|
1234
1635
|
};
|
|
1235
1636
|
}
|
|
1236
1637
|
function planFallbacks(members, detected) {
|
|
@@ -1510,18 +1911,26 @@ export async function listProviderModels(id, det) {
|
|
|
1510
1911
|
const listing = await describeProviderModels(id, det);
|
|
1511
1912
|
return listing.kind === 'ok' ? [...listing.models] : [];
|
|
1512
1913
|
}
|
|
1513
|
-
/**
|
|
1914
|
+
/** The newest user turn's text, for labels. */
|
|
1915
|
+
function lastUserText(messages) {
|
|
1916
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1917
|
+
const m = messages[i];
|
|
1918
|
+
if (m?.role === 'user' && typeof m.content === 'string')
|
|
1919
|
+
return m.content;
|
|
1920
|
+
}
|
|
1921
|
+
return '';
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* A scope for a session no host supplied one for: four minted ids. Minted
|
|
1925
|
+
* rather than spelled, because a spelled id is a place a typo hides and
|
|
1926
|
+
* these types accept either spelling until they are nominal.
|
|
1927
|
+
*/
|
|
1514
1928
|
function mintScope() {
|
|
1515
|
-
const suffix = `tui-${Date.now().toString(36)}`;
|
|
1516
|
-
// Through the constructors rather than as four bare template literals.
|
|
1517
|
-
// One suffix shared by four ids is exactly the shape a typo hides in —
|
|
1518
|
-
// `top_` and `tnt_` differ by two characters, and the types accept either
|
|
1519
|
-
// spelling for either field while they are still structural.
|
|
1520
1929
|
return {
|
|
1521
|
-
sessionId:
|
|
1522
|
-
topicId:
|
|
1523
|
-
projectId:
|
|
1524
|
-
tenantId:
|
|
1930
|
+
sessionId: generateSessionId(),
|
|
1931
|
+
topicId: generateTopicId(),
|
|
1932
|
+
projectId: generateProjectId(),
|
|
1933
|
+
tenantId: generateTenantId(),
|
|
1525
1934
|
};
|
|
1526
1935
|
}
|
|
1527
1936
|
// Pre-execution safety gate: hard-deny catastrophic shell patterns
|
|
@@ -1560,7 +1969,7 @@ function gateFor(rules) {
|
|
|
1560
1969
|
// from `runConfig.model`, which is the value the user actually chose. Pinning
|
|
1561
1970
|
// a number here would fix one window across every model the CLI can talk to.
|
|
1562
1971
|
const COMPACTION_CONFIG = {
|
|
1563
|
-
strategy: '
|
|
1972
|
+
strategy: 'salience',
|
|
1564
1973
|
// On, and this is the CLI making a choice rather than taking a default.
|
|
1565
1974
|
// A session's transcript is the only record of what was compacted away;
|
|
1566
1975
|
// the size trade this costs is the operator's to see and turn off.
|
|
@@ -1588,7 +1997,17 @@ const COMPACTION_CONFIG = {
|
|
|
1588
1997
|
maxCharsPerRequirement: 300,
|
|
1589
1998
|
maxCharsPerTask: 400,
|
|
1590
1999
|
};
|
|
1591
|
-
|
|
2000
|
+
/** The shipped configuration with the strategy the project chose, if it chose one. */
|
|
2001
|
+
function compactionConfigFor(compaction) {
|
|
2002
|
+
return {
|
|
2003
|
+
...COMPACTION_CONFIG,
|
|
2004
|
+
strategy: compaction?.strategy ?? COMPACTION_CONFIG.strategy,
|
|
2005
|
+
...(compaction?.contextWindowTokens !== undefined
|
|
2006
|
+
? { contextWindowTokens: compaction.contextWindowTokens }
|
|
2007
|
+
: {}),
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
async function* runTurn({ provider, compactionConfig, consolidateInto, backgroundJobs, backgroundJobOwner, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, pathBuilder, workingDirectory, limits, additionalDirectories, sandboxWorkspace, rules, reviewAnswer, maxAnswerReviews, promoteMemory, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, promptContributions, runtimeToolOverrides, web, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
|
|
1592
2011
|
const signal = opts?.signal;
|
|
1593
2012
|
// One presenter for the whole stream, built from the registry this scope
|
|
1594
2013
|
// already holds. Its absence HERE is what forced presentation to be name
|
|
@@ -1612,7 +2031,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1612
2031
|
// turn. An admitted send owns the exact run-scoped authority above.
|
|
1613
2032
|
...(!opts?.goalRound ? { deniedTools: SESSION_GOAL_TOOL_NAMES } : {}),
|
|
1614
2033
|
taskStore,
|
|
1615
|
-
...(taskGateway ? { taskGateway } : {}),
|
|
2034
|
+
...(taskGateway ? { taskScheduler: taskGateway } : {}),
|
|
1616
2035
|
// `gateFor`, not the bare default: the default's `rules` is a hardcoded
|
|
1617
2036
|
// empty array, so passing it here discarded the operator's rules on the
|
|
1618
2037
|
// path that runs every top-level turn. The sub-agent path called
|
|
@@ -1620,7 +2039,9 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1620
2039
|
...(sandboxProvider ? { sandboxProvider } : {}),
|
|
1621
2040
|
...(sandboxTeardownTimeoutMs !== undefined ? { sandboxTeardownTimeoutMs } : {}),
|
|
1622
2041
|
authorizationGate: gateFor(rules),
|
|
1623
|
-
compactionConfig
|
|
2042
|
+
compactionConfig,
|
|
2043
|
+
...(consolidateInto ? { consolidateInto } : {}),
|
|
2044
|
+
...(backgroundJobs ? { backgroundJobs, backgroundJobOwner } : {}),
|
|
1624
2045
|
// The CLI owns its process end to end, so it can safely hand the
|
|
1625
2046
|
// termination path to the kernel: a Ctrl-C mid-run now leaves a
|
|
1626
2047
|
// dump under the injected hierarchy's emergency partition instead of
|
|
@@ -1631,8 +2052,8 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1631
2052
|
...(sandboxProvider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1632
2053
|
...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
|
|
1633
2054
|
timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
1634
|
-
tokenBudget: 1_000_000,
|
|
1635
|
-
maxIterations: 50,
|
|
2055
|
+
tokenBudget: limits?.tokenBudget ?? 1_000_000,
|
|
2056
|
+
maxIterations: limits?.maxIterations ?? 50,
|
|
1636
2057
|
maxResponseTokens: 8192,
|
|
1637
2058
|
permissionMode: 'auto',
|
|
1638
2059
|
},
|
|
@@ -1653,10 +2074,14 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1653
2074
|
messages: [...messages],
|
|
1654
2075
|
...(opts?.inboundMessages ? { inboundMessages: opts.inboundMessages } : {}),
|
|
1655
2076
|
workingDirectory,
|
|
2077
|
+
...(additionalDirectories?.length ? { additionalDirectories } : {}),
|
|
1656
2078
|
// The exemption reads `tools` at decision time, so it sees the task
|
|
1657
2079
|
// tools `query()` registers deferred below and any tool server that
|
|
1658
2080
|
// connected after this session was built.
|
|
1659
2081
|
resumeHandler,
|
|
2082
|
+
...(promptContributions ? { promptContributions } : {}),
|
|
2083
|
+
...(runtimeToolOverrides ? { runtimeToolOverrides } : {}),
|
|
2084
|
+
...(web ? { web } : {}),
|
|
1660
2085
|
signal,
|
|
1661
2086
|
...scope,
|
|
1662
2087
|
});
|
|
@@ -1706,123 +2131,26 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1706
2131
|
}
|
|
1707
2132
|
}
|
|
1708
2133
|
/**
|
|
1709
|
-
*
|
|
1710
|
-
* callback. Read-only batches (nothing destructive) run silently; batches
|
|
1711
|
-
* with a destructive call prompt the user unless they've already chosen
|
|
1712
|
-
* "approve all" for the session. Plans and iteration checkpoints are
|
|
1713
|
-
* auto-continued (the TUI doesn't use plan mode).
|
|
1714
|
-
*/
|
|
1715
|
-
export function makeResumeHandler(approval, onPermission, mode = onPermission ? 'prompt' : 'auto',
|
|
1716
|
-
/**
|
|
1717
|
-
* Which calls skip the prompt. Injected rather than reached for, so this
|
|
1718
|
-
* handler stays testable without a registry — and so the answer comes from
|
|
1719
|
-
* the live roster at the moment of the call.
|
|
1720
|
-
*/
|
|
1721
|
-
exempt = () => false) {
|
|
1722
|
-
return async (request) => {
|
|
1723
|
-
if (request.type !== 'tool_review') {
|
|
1724
|
-
return request.type === 'plan_approval' ? { action: 'approve_plan' } : { action: 'continue' };
|
|
1725
|
-
}
|
|
1726
|
-
// Only calls the gate routed to REVIEW arrive here — a rule that denied
|
|
1727
|
-
// one already stopped it, and a rule that allowed one never asked. So the
|
|
1728
|
-
// mode decides what happens to the undecided, and cannot reopen anything
|
|
1729
|
-
// a rule closed. That is the whole precedence story between a flag and a
|
|
1730
|
-
// config file, and it is one sentence on purpose.
|
|
1731
|
-
if (!batchNeedsPrompt(request.toolCalls, exempt)) {
|
|
1732
|
-
return { action: 'approve_tools' };
|
|
1733
|
-
}
|
|
1734
|
-
if (mode === 'strict') {
|
|
1735
|
-
return {
|
|
1736
|
-
action: 'reject_tools',
|
|
1737
|
-
feedback: 'Refused: this run only permits tools an explicit rule allows, and no rule covers this call. Asking again will not change it — either the operator adds a rule, or this has to be done another way.',
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
if (mode === 'auto' || !onPermission || approval.all) {
|
|
1741
|
-
return { action: 'approve_tools' };
|
|
1742
|
-
}
|
|
1743
|
-
const decision = await onPermission({
|
|
1744
|
-
toolCalls: request.toolCalls.map((tc) => ({
|
|
1745
|
-
id: tc.id,
|
|
1746
|
-
name: tc.name,
|
|
1747
|
-
input: tc.input,
|
|
1748
|
-
isDestructive: tc.isDestructive,
|
|
1749
|
-
})),
|
|
1750
|
-
});
|
|
1751
|
-
switch (decision.kind) {
|
|
1752
|
-
case 'approve':
|
|
1753
|
-
return { action: 'approve_tools' };
|
|
1754
|
-
case 'approve-all':
|
|
1755
|
-
approval.all = true;
|
|
1756
|
-
return { action: 'approve_tools' };
|
|
1757
|
-
case 'reject':
|
|
1758
|
-
return {
|
|
1759
|
-
action: 'reject_tools',
|
|
1760
|
-
feedback: decision.feedback ?? 'User declined to run the proposed tool(s).',
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
};
|
|
1764
|
-
}
|
|
1765
|
-
/**
|
|
1766
|
-
* Writes that skip the prompt anyway, in spite of declaring `readOnly: false`.
|
|
1767
|
-
*
|
|
1768
|
-
* This is an OVERRIDE of the tool's own declaration, and it is named as one.
|
|
1769
|
-
* The list it replaced was called `READ_ONLY_TOOLS` and contained three tools
|
|
1770
|
-
* that declare `readOnly: false` — a constant asserting the exact property it
|
|
1771
|
-
* was getting wrong, which is how the disagreement survived: nothing reading it
|
|
1772
|
-
* had reason to doubt the name.
|
|
2134
|
+
* The kernel's review policy with the TUI's prompt behind it.
|
|
1773
2135
|
*
|
|
1774
|
-
* The
|
|
1775
|
-
*
|
|
1776
|
-
*
|
|
1777
|
-
*
|
|
1778
|
-
* - `task_create` / `task_update` — the model's own plan for the current
|
|
1779
|
-
* request, written several times per planning turn; prompting each would put
|
|
1780
|
-
* a consent dialog between the agent and its todo list. What a bad write
|
|
1781
|
-
* costs is a polluted task list, which is visible in the transcript and
|
|
1782
|
-
* grants nothing. Worth knowing while reading that: these DO outlive the
|
|
1783
|
-
* session, because the CLI's task store uses a fixed run id
|
|
1784
|
-
* (`run_namzu-cli`), so "run-scoped" is not the reason they are here — the
|
|
1785
|
-
* blast radius is.
|
|
1786
|
-
*
|
|
1787
|
-
* `save_memory` was on the list it replaced and is deliberately NOT here. Its
|
|
1788
|
-
* effect outlives the run in a way the task tools' does not: content saved now
|
|
1789
|
-
* is retrievable by `search_memory` in a later session, so a tool result or
|
|
1790
|
-
* fetched page that talks the model into saving something reaches a future
|
|
1791
|
-
* run's reasoning. It is not auto-injected into the prompt — that is
|
|
1792
|
-
* `MEMORY.md`, a different thing — but retrievable is enough. A write that
|
|
1793
|
-
* survives the process, into the user's own repository, is not read-only under
|
|
1794
|
-
* any reading, and it now prompts.
|
|
2136
|
+
* The five modes, the exemptions and the batch rule live in `@namzu/sdk`
|
|
2137
|
+
* (`createReviewHandler`); what this application adds is the person to ask
|
|
2138
|
+
* and the session's "approve all" box, which the screen also reads.
|
|
1795
2139
|
*/
|
|
1796
|
-
|
|
2140
|
+
export function makeResumeHandler(approval, onPermission, mode = onPermission ? 'prompt' : 'auto', exempt = () => false) {
|
|
2141
|
+
return createReviewHandler({
|
|
2142
|
+
mode,
|
|
2143
|
+
prompt: onPermission,
|
|
2144
|
+
exempt,
|
|
2145
|
+
remembered: approval,
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
1797
2148
|
/**
|
|
1798
|
-
* Whether a call runs without asking:
|
|
1799
|
-
* named
|
|
1800
|
-
*
|
|
1801
|
-
* The read-only half comes from the tool's own `isReadOnly(input)`, never from
|
|
1802
|
-
* a list of names kept here. A name list in the consumer is a second source of
|
|
1803
|
-
* truth for a property the producer already states: a new read-only tool
|
|
1804
|
-
* missing from it merely gets prompted, but a RENAMED tool silently changes
|
|
1805
|
-
* posture with nothing to notice.
|
|
1806
|
-
*
|
|
1807
|
-
* Resolved per call rather than snapshotted, because the roster changes after
|
|
1808
|
-
* this module has run — the task tools are registered deferred inside
|
|
1809
|
-
* `query()`, and tool servers connect during startup, so anything computed
|
|
1810
|
-
* eagerly would be answering about a registry that no longer exists.
|
|
1811
|
-
*
|
|
1812
|
-
* A tool the registry does not know, or one that declares nothing, prompts.
|
|
1813
|
-
* That is the safe-by-default direction the previous comment claimed and this
|
|
1814
|
-
* keeps: consent is the answer when the question cannot be established.
|
|
2149
|
+
* Whether a call runs without asking. The kernel's rule: a trusted read-only
|
|
2150
|
+
* declaration or a named bookkeeping write, never a fetch, never a tool the
|
|
2151
|
+
* registry does not know.
|
|
1815
2152
|
*/
|
|
1816
|
-
export
|
|
1817
|
-
if (PROMPT_EXEMPT_WRITES.has(name.toLowerCase()))
|
|
1818
|
-
return true;
|
|
1819
|
-
const tool = registry.get(name) ?? registry.get(name.toLowerCase());
|
|
1820
|
-
// A connected server's own claim about its own tool cannot skip the
|
|
1821
|
-
// prompt. Same predicate the kernel gate and plan mode use -- three
|
|
1822
|
-
// doors, one rule, because fixing two would close the issue and leave
|
|
1823
|
-
// the boundary open.
|
|
1824
|
-
return isTrustedReadOnly(tool, input);
|
|
1825
|
-
}
|
|
2153
|
+
export const isPromptExempt = isReviewExempt;
|
|
1826
2154
|
/** The exempt roster, sorted, for the surface that has to NAME it. */
|
|
1827
2155
|
export function promptExemptToolNames(registry) {
|
|
1828
2156
|
return registry
|
|
@@ -1831,13 +2159,8 @@ export function promptExemptToolNames(registry) {
|
|
|
1831
2159
|
.map((t) => t.name)
|
|
1832
2160
|
.sort();
|
|
1833
2161
|
}
|
|
1834
|
-
/**
|
|
1835
|
-
|
|
1836
|
-
* destructive by the SDK, or not exempt from the prompt.
|
|
1837
|
-
*/
|
|
1838
|
-
export function batchNeedsPrompt(toolCalls, exempt) {
|
|
1839
|
-
return toolCalls.some((tc) => tc.isDestructive || !exempt(tc.name, tc.input));
|
|
1840
|
-
}
|
|
2162
|
+
/** A batch needs explicit approval when any call mutates state. */
|
|
2163
|
+
export const batchNeedsPrompt = batchNeedsReview;
|
|
1841
2164
|
/**
|
|
1842
2165
|
* Translate one SDK `RunEvent` into the TUI's `AgentEvent` vocabulary, or
|
|
1843
2166
|
* `null` for events the chat surface doesn't render (iteration markers,
|
|
@@ -1852,6 +2175,14 @@ export function toAgentEvent(event, presenter) {
|
|
|
1852
2175
|
...(event.messageId ? { messageId: event.messageId } : {}),
|
|
1853
2176
|
...(event.runId ? { runId: event.runId } : {}),
|
|
1854
2177
|
};
|
|
2178
|
+
case 'reasoning_started':
|
|
2179
|
+
// A redacted block has no text to show; the empty delta still says
|
|
2180
|
+
// "thinking" so the region does not sit silent for its duration.
|
|
2181
|
+
return { kind: 'reasoning', text: '' };
|
|
2182
|
+
case 'reasoning_delta':
|
|
2183
|
+
return { kind: 'reasoning', text: event.text };
|
|
2184
|
+
case 'reasoning_completed':
|
|
2185
|
+
return { kind: 'reasoning', text: '', done: true };
|
|
1855
2186
|
case 'tool_executing':
|
|
1856
2187
|
return {
|
|
1857
2188
|
kind: 'tool-start',
|
|
@@ -1883,9 +2214,15 @@ export function toAgentEvent(event, presenter) {
|
|
|
1883
2214
|
success: !event.isError,
|
|
1884
2215
|
output: event.result,
|
|
1885
2216
|
});
|
|
1886
|
-
const summary = firstLine(event.result);
|
|
1887
2217
|
const detail = viewToLines(view);
|
|
1888
|
-
|
|
2218
|
+
// For output shown line by line, the summary IS the first rendered
|
|
2219
|
+
// line, so the body can drop it without a second, differently
|
|
2220
|
+
// whitespaced copy of the same text — a `read` used to show its
|
|
2221
|
+
// first line twice, once collapsed and once numbered.
|
|
2222
|
+
const summary = view.kind === 'terminal' && detail && detail.length > 0
|
|
2223
|
+
? truncate(detail[0], 120)
|
|
2224
|
+
: firstLine(event.result);
|
|
2225
|
+
const withoutRepeatedSummary = view.kind === 'terminal' && detail && detail.length > 0 ? detail.slice(1) : detail;
|
|
1889
2226
|
return {
|
|
1890
2227
|
kind: 'tool-end',
|
|
1891
2228
|
runId: event.runId,
|
|
@@ -1917,6 +2254,7 @@ export function toAgentEvent(event, presenter) {
|
|
|
1917
2254
|
return {
|
|
1918
2255
|
kind: 'usage',
|
|
1919
2256
|
totalTokens: event.usage.totalTokens,
|
|
2257
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
1920
2258
|
cost: event.cost,
|
|
1921
2259
|
...(event.contextTokens !== undefined ? { contextTokens: event.contextTokens } : {}),
|
|
1922
2260
|
...(event.contextMeasuredBy !== undefined
|
|
@@ -1963,13 +2301,16 @@ export function toAgentEvent(event, presenter) {
|
|
|
1963
2301
|
};
|
|
1964
2302
|
}
|
|
1965
2303
|
case 'task_created':
|
|
1966
|
-
return { kind: 'task', subject: event.subject, status: event.status };
|
|
1967
2304
|
case 'task_updated':
|
|
1968
|
-
//
|
|
1969
|
-
//
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
:
|
|
2305
|
+
// Every change, not only completions: the live task list needs the
|
|
2306
|
+
// in-progress flips to show which step is current. The transcript
|
|
2307
|
+
// decides for itself which of these it records.
|
|
2308
|
+
return {
|
|
2309
|
+
kind: 'task',
|
|
2310
|
+
taskId: String(event.taskId),
|
|
2311
|
+
subject: event.subject,
|
|
2312
|
+
status: event.status,
|
|
2313
|
+
};
|
|
1973
2314
|
case 'run_paused':
|
|
1974
2315
|
// A pause is not an error and not an invisible end. The checkpoint and
|
|
1975
2316
|
// classification are the recovery surface; dropping this event made a
|
|
@@ -1977,6 +2318,8 @@ export function toAgentEvent(event, presenter) {
|
|
|
1977
2318
|
// the SDK had explicitly stopped.
|
|
1978
2319
|
return {
|
|
1979
2320
|
kind: 'paused',
|
|
2321
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2322
|
+
runId: String(event.runId),
|
|
1980
2323
|
checkpointId: event.checkpointId,
|
|
1981
2324
|
reason: event.reason,
|
|
1982
2325
|
...(event.failure ? { failure: event.failure } : {}),
|
|
@@ -1991,6 +2334,7 @@ export function toAgentEvent(event, presenter) {
|
|
|
1991
2334
|
// answer was refused.
|
|
1992
2335
|
return {
|
|
1993
2336
|
kind: 'done',
|
|
2337
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
1994
2338
|
...(event.stopReason ? { stopReason: event.stopReason } : {}),
|
|
1995
2339
|
};
|
|
1996
2340
|
case 'run_failed':
|
|
@@ -2000,13 +2344,29 @@ export function toAgentEvent(event, presenter) {
|
|
|
2000
2344
|
// still forcing every host to parse prose.
|
|
2001
2345
|
return {
|
|
2002
2346
|
kind: 'error',
|
|
2347
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2003
2348
|
message: event.error,
|
|
2004
2349
|
...(event.failure ? { failure: event.failure } : {}),
|
|
2005
2350
|
...(event.providerError ? { providerError: event.providerError } : {}),
|
|
2006
2351
|
...(event.explanation ? { explanation: event.explanation } : {}),
|
|
2007
2352
|
};
|
|
2353
|
+
case 'background_job_exited':
|
|
2354
|
+
return {
|
|
2355
|
+
kind: 'job',
|
|
2356
|
+
jobId: event.jobId,
|
|
2357
|
+
command: event.command,
|
|
2358
|
+
status: event.status,
|
|
2359
|
+
...(event.exitCode !== undefined ? { exitCode: event.exitCode } : {}),
|
|
2360
|
+
...(event.signal ? { signal: event.signal } : {}),
|
|
2361
|
+
};
|
|
2008
2362
|
case 'compaction_completed':
|
|
2009
|
-
return {
|
|
2363
|
+
return {
|
|
2364
|
+
kind: 'context',
|
|
2365
|
+
text: describeCompaction(event),
|
|
2366
|
+
shed: true,
|
|
2367
|
+
summarised: true,
|
|
2368
|
+
reclaimedTokens: Math.max(0, event.tokensBefore - event.tokensAfter),
|
|
2369
|
+
};
|
|
2010
2370
|
case 'compaction_tool_results_cleared':
|
|
2011
2371
|
// `shed: true` on both branches: the tool-result bodies are gone
|
|
2012
2372
|
// either way. `reliefWasEnough: false` additionally means a
|
|
@@ -2014,8 +2374,11 @@ export function toAgentEvent(event, presenter) {
|
|
|
2014
2374
|
// so this one says what IT cost rather than claiming the total.
|
|
2015
2375
|
return {
|
|
2016
2376
|
kind: 'context',
|
|
2017
|
-
text: `cleared ${event.clearedCount}
|
|
2377
|
+
text: `cleared ${event.clearedCount} tool result${event.clearedCount === 1 ? '' : 's'}${event.stubbedCount ? `, stubbed ${event.stubbedCount} narration${event.stubbedCount === 1 ? '' : 's'}` : ''} (~${event.reclaimedTokens.toLocaleString()} tokens)${event.reliefWasEnough ? '' : ' — not enough, compacting'}`,
|
|
2018
2378
|
shed: true,
|
|
2379
|
+
cleared: event.clearedCount,
|
|
2380
|
+
stubbed: event.stubbedCount ?? 0,
|
|
2381
|
+
reclaimedTokens: event.reclaimedTokens,
|
|
2019
2382
|
};
|
|
2020
2383
|
case 'compaction_failed':
|
|
2021
2384
|
return {
|
|
@@ -2355,6 +2718,9 @@ function emptySession(errorHint, errorKind = 'environment') {
|
|
|
2355
2718
|
resumeDurable: async () => {
|
|
2356
2719
|
throw new Error(errorHint);
|
|
2357
2720
|
},
|
|
2721
|
+
resumePaused: async function* () {
|
|
2722
|
+
yield { kind: 'error', message: 'no provider: nothing to resume' };
|
|
2723
|
+
},
|
|
2358
2724
|
close: async () => {
|
|
2359
2725
|
// Nothing was ever connected on this path.
|
|
2360
2726
|
},
|