@namzu/cli 18.1.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 +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +2 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +15 -2
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/provider-wait.d.ts +44 -0
- package/dist/commands/provider-wait.d.ts.map +1 -0
- package/dist/commands/provider-wait.js +65 -0
- package/dist/commands/provider-wait.js.map +1 -0
- package/dist/commands/run-flags.d.ts +13 -1
- package/dist/commands/run-flags.d.ts.map +1 -1
- package/dist/commands/run-flags.js +41 -0
- package/dist/commands/run-flags.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +33 -13
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +132 -78
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/stdin.d.ts +17 -0
- package/dist/commands/stdin.d.ts.map +1 -0
- package/dist/commands/stdin.js +69 -0
- package/dist/commands/stdin.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +107 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +80 -5
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/doctrine.d.ts +16 -38
- package/dist/context/doctrine.d.ts.map +1 -1
- package/dist/context/doctrine.js +16 -75
- package/dist/context/doctrine.js.map +1 -1
- package/dist/context/environment.d.ts +2 -0
- package/dist/context/environment.d.ts.map +1 -1
- package/dist/context/environment.js +3 -0
- package/dist/context/environment.js.map +1 -1
- package/dist/integrations/mcp/servers.d.ts +19 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +23 -2
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/plugins/runtime.d.ts +8 -2
- package/dist/integrations/plugins/runtime.d.ts.map +1 -1
- package/dist/integrations/plugins/runtime.js +24 -12
- package/dist/integrations/plugins/runtime.js.map +1 -1
- package/dist/integrations/providers/chain-capabilities.js +1 -1
- package/dist/integrations/providers/chain-capabilities.js.map +1 -1
- package/dist/integrations/providers/credential-store.d.ts +1 -1
- package/dist/integrations/providers/credential-store.js +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +7 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +2 -2
- package/dist/integrations/providers/registry.js +2 -2
- package/dist/integrations/sessions/store.d.ts +14 -22
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +78 -175
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +6 -2
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/state/identity.d.ts +21 -0
- package/dist/integrations/state/identity.d.ts.map +1 -0
- package/dist/integrations/state/identity.js +49 -0
- package/dist/integrations/state/identity.js.map +1 -0
- package/dist/integrations/state/immutable-json.d.ts +10 -0
- package/dist/integrations/state/immutable-json.d.ts.map +1 -0
- package/dist/integrations/state/immutable-json.js +38 -0
- package/dist/integrations/state/immutable-json.js.map +1 -0
- package/dist/integrations/state/project.d.ts +10 -0
- package/dist/integrations/state/project.d.ts.map +1 -0
- package/dist/integrations/state/project.js +15 -0
- package/dist/integrations/state/project.js.map +1 -0
- package/dist/integrations/state/report.d.ts.map +1 -1
- package/dist/integrations/state/report.js +42 -12
- package/dist/integrations/state/report.js.map +1 -1
- package/dist/integrations/subagents/definitions.d.ts +19 -0
- package/dist/integrations/subagents/definitions.d.ts.map +1 -0
- package/dist/integrations/subagents/definitions.js +26 -0
- package/dist/integrations/subagents/definitions.js.map +1 -0
- package/dist/integrations/subagents/parent.d.ts +13 -0
- package/dist/integrations/subagents/parent.d.ts.map +1 -0
- package/dist/integrations/subagents/parent.js +57 -0
- package/dist/integrations/subagents/parent.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +24 -13
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +282 -122
- 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 -45
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +18 -13
- package/dist/permissions/mode.js.map +1 -1
- package/dist/release-notes.d.ts +20 -0
- package/dist/release-notes.d.ts.map +1 -0
- package/dist/release-notes.js +64 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts.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 +606 -207
- 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 +4 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +4 -4
- 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 +108 -16
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +2 -2
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +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 +3 -1
- package/dist/tui/TaskList.d.ts.map +1 -1
- package/dist/tui/TaskList.js +13 -4
- package/dist/tui/TaskList.js.map +1 -1
- package/dist/tui/Transcript.d.ts +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 +113 -50
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +512 -293
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/choice-selection.d.ts +17 -0
- package/dist/tui/choice-selection.d.ts.map +1 -0
- package/dist/tui/choice-selection.js +38 -0
- package/dist/tui/choice-selection.js.map +1 -0
- package/dist/tui/exit-summary.d.ts +7 -1
- package/dist/tui/exit-summary.d.ts.map +1 -1
- package/dist/tui/exit-summary.js +12 -3
- package/dist/tui/exit-summary.js.map +1 -1
- package/dist/tui/index.d.ts +3 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -3
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/logo.d.ts +7 -22
- package/dist/tui/logo.d.ts.map +1 -1
- package/dist/tui/logo.js +8 -34
- package/dist/tui/logo.js.map +1 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +21 -6
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/permission-review.d.ts +15 -4
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +48 -9
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/provider-selection.d.ts +4 -0
- package/dist/tui/provider-selection.d.ts.map +1 -0
- package/dist/tui/provider-selection.js +16 -0
- package/dist/tui/provider-selection.js.map +1 -0
- package/dist/tui/run-interruption.d.ts +3 -2
- package/dist/tui/run-interruption.d.ts.map +1 -1
- package/dist/tui/run-interruption.js +2 -1
- package/dist/tui/run-interruption.js.map +1 -1
- package/dist/tui/shell-escape.d.ts +42 -0
- package/dist/tui/shell-escape.d.ts.map +1 -0
- package/dist/tui/shell-escape.js +110 -0
- package/dist/tui/shell-escape.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +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/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 +6 -1
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +6 -4
package/dist/tui/agent.js
CHANGED
|
@@ -20,10 +20,12 @@
|
|
|
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, GuardedFetchProvider, PromptContributionRegistry, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, WebFetchTool,
|
|
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';
|
|
28
30
|
import { NAMZU_DELEGATION_DOCTRINE, NAMZU_PLAN_MODE_DOCTRINE, NAMZU_WORKING_DOCTRINE, } from '../context/doctrine.js';
|
|
29
31
|
import { composeEnvironmentPrompt, readEnvironmentFacts } from '../context/environment.js';
|
|
@@ -34,11 +36,12 @@ import { connectMcpServers, } from '../integrations/mcp/servers.js';
|
|
|
34
36
|
import { createCliPluginRuntime } from '../integrations/plugins/runtime.js';
|
|
35
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';
|
|
36
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';
|
|
37
41
|
import { CLI_INTERACTIVE_RUN_TIMEOUT_MS } from '../integrations/subagents/policy.js';
|
|
38
42
|
import { createSubagentRuntime } from '../integrations/subagents/runtime.js';
|
|
39
43
|
import { cliLogger } from '../logging.js';
|
|
40
44
|
import { composeMemoryPrompt, readMemory } from '../memory/store.js';
|
|
41
|
-
import { ACCEPT_EDITS_TOOLS, PLAN_MODE_REFUSAL } from '../permissions/mode.js';
|
|
42
45
|
import { projectRunConversation } from './conversation-history.js';
|
|
43
46
|
/**
|
|
44
47
|
* Let one caller stop waiting without cutting a shared queue in the middle.
|
|
@@ -241,7 +244,17 @@ async function drainIterator(iterator) {
|
|
|
241
244
|
*/
|
|
242
245
|
export async function probeAgentSession() {
|
|
243
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');
|
|
244
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
|
+
});
|
|
245
258
|
switch (read.status) {
|
|
246
259
|
case 'ok':
|
|
247
260
|
return {
|
|
@@ -360,9 +373,21 @@ function builtinTools(backgroundJobs) {
|
|
|
360
373
|
return [tool.name === 'bash' ? foregroundOnlyBash(tool) : tool];
|
|
361
374
|
});
|
|
362
375
|
}
|
|
363
|
-
function buildToolRegistry(cwd, projectStateRoot = join(cwd, '.namzu'), backgroundJobs = true) {
|
|
376
|
+
function buildToolRegistry(cwd, projectStateRoot = join(cwd, '.namzu'), backgroundJobs = true, checkpoints) {
|
|
364
377
|
const registry = new ToolRegistry();
|
|
365
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
|
+
}
|
|
366
391
|
// SDK memory: the agent gets search_memory / read_memory / save_memory over
|
|
367
392
|
// a structured store in this Project's generated-state directory. CLI
|
|
368
393
|
// surfaces inject the central application-home hierarchy; embedded callers
|
|
@@ -714,8 +739,46 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
714
739
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.SANDBOX_RESOLVED,
|
|
715
740
|
'namzu.sandbox.unconfined': sandbox.unconfined,
|
|
716
741
|
});
|
|
717
|
-
|
|
718
|
-
|
|
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);
|
|
719
782
|
// Package presence is not tool reachability. The CLI used to probe and
|
|
720
783
|
// report @namzu/computer-use without ever constructing its host or mounting
|
|
721
784
|
// SDK's computer_use definition, so even an installed, healthy package was
|
|
@@ -735,6 +798,28 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
735
798
|
catch (error) {
|
|
736
799
|
computerUseError = error instanceof Error ? error : new Error(String(error));
|
|
737
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
|
+
}));
|
|
738
823
|
}
|
|
739
824
|
}
|
|
740
825
|
// Registered only on the main session path. Sub-agents call
|
|
@@ -804,7 +889,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
804
889
|
// Native sub-agents: register the canonical `Agent` tool so the model can
|
|
805
890
|
// delegate a self-contained task to a fresh sub-agent (own context window).
|
|
806
891
|
// Best-effort — if the runtime can't stand up, the chat still works.
|
|
807
|
-
|
|
892
|
+
const delegationScopes = new Map();
|
|
808
893
|
let subagentRuntime;
|
|
809
894
|
// Stays empty when the runtime below throws, which is the honest answer: the
|
|
810
895
|
// catch is non-fatal and the session then genuinely has no delegate to
|
|
@@ -812,10 +897,28 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
812
897
|
// would name agents that are not there.
|
|
813
898
|
let allowedAgentIds = [];
|
|
814
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
|
+
}
|
|
815
910
|
const sub = await createSubagentRuntime({
|
|
816
911
|
cwd,
|
|
817
912
|
model,
|
|
818
|
-
|
|
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
|
+
},
|
|
819
922
|
sandboxWorkspace,
|
|
820
923
|
resolveResumeHandler: (runId) => delegatedResumeHandlers.get(runId),
|
|
821
924
|
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
@@ -862,7 +965,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
862
965
|
});
|
|
863
966
|
subagentRuntime = sub;
|
|
864
967
|
registry.register([sub.agentTool]);
|
|
865
|
-
subagentGateway = sub.gateway;
|
|
866
968
|
allowedAgentIds = sub.allowedAgentIds;
|
|
867
969
|
}
|
|
868
970
|
catch (err) {
|
|
@@ -883,7 +985,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
883
985
|
// gateway the tool builder requires; a session without one has no
|
|
884
986
|
// question tool either, and says nothing — it also has no `Agent`.
|
|
885
987
|
let currentOnQuestion;
|
|
886
|
-
if (options.askUser &&
|
|
988
|
+
if (options.askUser && subagentRuntime) {
|
|
887
989
|
const parkQuestion = async (request) => {
|
|
888
990
|
if (request.type !== 'user_question')
|
|
889
991
|
return { action: 'continue' };
|
|
@@ -905,20 +1007,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
905
1007
|
return { action: 'continue' };
|
|
906
1008
|
}
|
|
907
1009
|
};
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
allowDelegation: false,
|
|
913
|
-
resumeHandler: parkQuestion,
|
|
914
|
-
// The builder stamps this on the park request. The handler above
|
|
915
|
-
// routes by the question, not by the run, and no durable park
|
|
916
|
-
// recorder is supplied, so a session-scoped id is what is true: the
|
|
917
|
-
// tool is built once per session and the turn is not known yet.
|
|
918
|
-
runId: asRunId('run_namzu-interactive-question'),
|
|
919
|
-
}).find((tool) => tool.name === 'ask_user_question');
|
|
920
|
-
if (askTool)
|
|
921
|
-
registry.register(askTool);
|
|
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 }));
|
|
922
1014
|
}
|
|
923
1015
|
// Task store → query registers task_create / task_update / task_list and
|
|
924
1016
|
// emits task_created/task_updated, so the agent can track a plan for the
|
|
@@ -937,11 +1029,39 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
937
1029
|
// captured on this line. The count at connect time is unchanged; what
|
|
938
1030
|
// changes is that asking again later gets a later answer.
|
|
939
1031
|
ensurePrivateStateDirectory(projectStateRoot, 'tenants');
|
|
940
|
-
const
|
|
1032
|
+
const taskStoreForRun = (runId, tenantId) => new DiskTaskStore({
|
|
941
1033
|
baseDir: projectStateRoot,
|
|
942
|
-
defaultRunId:
|
|
943
|
-
tenantId
|
|
1034
|
+
defaultRunId: runId,
|
|
1035
|
+
tenantId,
|
|
944
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
|
+
};
|
|
945
1065
|
// Persists across turns: once the user picks "approve all", later tool
|
|
946
1066
|
// batches in this session run without prompting.
|
|
947
1067
|
const approval = { all: false };
|
|
@@ -963,12 +1083,31 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
963
1083
|
// registries, so executable plugins remain a top-level-session capability.
|
|
964
1084
|
let pluginRuntime;
|
|
965
1085
|
try {
|
|
966
|
-
pluginRuntime = await createCliPluginRuntime(options.plugins, registry, cwd);
|
|
1086
|
+
pluginRuntime = await createCliPluginRuntime(options.plugins, registry, cwd, options.hooks);
|
|
967
1087
|
}
|
|
968
1088
|
catch (error) {
|
|
969
1089
|
await Promise.allSettled([mcp.close(), computerUseHost?.dispose()]);
|
|
970
1090
|
return emptySession(describeError(error));
|
|
971
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
|
+
};
|
|
972
1111
|
if (pluginRuntime) {
|
|
973
1112
|
cliLogger().info('discovery complete', {
|
|
974
1113
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.DISCOVERY_COMPLETED,
|
|
@@ -990,9 +1129,19 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
990
1129
|
const operations = new SessionOperationOwner(async () => {
|
|
991
1130
|
const results = await Promise.allSettled([
|
|
992
1131
|
subagentRuntime?.close?.(),
|
|
993
|
-
|
|
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,
|
|
994
1141
|
mcp.close(),
|
|
995
1142
|
computerUseHost?.dispose(),
|
|
1143
|
+
jobRegistry?.killOwner(jobOwner),
|
|
1144
|
+
checkpoints.close(),
|
|
996
1145
|
]);
|
|
997
1146
|
const failures = results
|
|
998
1147
|
.filter((result) => result.status === 'rejected')
|
|
@@ -1032,6 +1181,200 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1032
1181
|
reasoningEffortDefault = undefined;
|
|
1033
1182
|
effortNotice = `Reasoning effort levels could not be established for this session: ${describeError(error)}`;
|
|
1034
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
|
+
};
|
|
1035
1378
|
return {
|
|
1036
1379
|
hasProvider: true,
|
|
1037
1380
|
sandbox: {
|
|
@@ -1049,7 +1392,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1049
1392
|
await prepareProviderCredential(signal);
|
|
1050
1393
|
return compactNow({
|
|
1051
1394
|
messages,
|
|
1052
|
-
config:
|
|
1395
|
+
config: compactionConfigFor(options.compaction),
|
|
1053
1396
|
provider,
|
|
1054
1397
|
model,
|
|
1055
1398
|
signal,
|
|
@@ -1063,6 +1406,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1063
1406
|
.map((t) => t.name)
|
|
1064
1407
|
.filter((name) => !goalToolNames.has(name)),
|
|
1065
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
|
+
}) ?? (() => { }),
|
|
1066
1419
|
...(subagentRuntime ? { subagents: subagentRuntime.activity } : {}),
|
|
1067
1420
|
get instructionFiles() {
|
|
1068
1421
|
return projectInstructions.instructionFiles;
|
|
@@ -1096,6 +1449,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1096
1449
|
},
|
|
1097
1450
|
promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
|
|
1098
1451
|
send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
|
|
1452
|
+
const selectTaskStore = beginTaskStoreReadout();
|
|
1099
1453
|
const runId = opts?.runId ?? generateRunId();
|
|
1100
1454
|
const turnOpts = { ...opts, runId, signal };
|
|
1101
1455
|
const resumeHandler = makeResumeHandler(approval, opts?.onPermission, opts?.permissionMode ?? options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
@@ -1103,6 +1457,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1103
1457
|
throw new Error(`Run ${runId} already owns a delegated review channel.`);
|
|
1104
1458
|
}
|
|
1105
1459
|
delegatedResumeHandlers.set(runId, resumeHandler);
|
|
1460
|
+
const turnScope = { ...scope };
|
|
1461
|
+
delegationScopes.set(runId, turnScope);
|
|
1106
1462
|
try {
|
|
1107
1463
|
// Renew a lapsed OAuth token before the turn runs (no-op for valid
|
|
1108
1464
|
// tokens and non-subscription credentials).
|
|
@@ -1121,13 +1477,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1121
1477
|
const pluginSkills = pluginRuntime
|
|
1122
1478
|
? await currentPluginSkills(pluginRuntime.skills)
|
|
1123
1479
|
: undefined;
|
|
1124
|
-
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
1480
|
+
const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
|
|
1125
1481
|
currentOnQuestion = opts?.onQuestion;
|
|
1126
1482
|
const [environmentFacts, turnSnapshot] = await Promise.all([
|
|
1127
1483
|
readEnvironmentFacts(cwd),
|
|
1128
1484
|
readTurnSnapshot(cwd),
|
|
1129
1485
|
]);
|
|
1130
|
-
const environmentPrompt = composeEnvironmentPrompt(
|
|
1486
|
+
const environmentPrompt = composeEnvironmentPrompt({
|
|
1487
|
+
...environmentFacts,
|
|
1488
|
+
additionalDirectories: [...directories],
|
|
1489
|
+
});
|
|
1131
1490
|
// The repository as it stood when THIS turn began, through the
|
|
1132
1491
|
// SDK's `turn` placement — the ephemeral trailing message that is
|
|
1133
1492
|
// never cached and never enters history. FIRST iteration only:
|
|
@@ -1162,6 +1521,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1162
1521
|
]
|
|
1163
1522
|
.filter((s) => Boolean(s))
|
|
1164
1523
|
.join('\n\n') || undefined;
|
|
1524
|
+
await announceSessionStart();
|
|
1525
|
+
checkpoints.beginTurn(lastUserText(messages));
|
|
1165
1526
|
let capturedAuthority;
|
|
1166
1527
|
if (opts?.goalRound) {
|
|
1167
1528
|
if (!opts.runId)
|
|
@@ -1191,6 +1552,14 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1191
1552
|
try {
|
|
1192
1553
|
yield* runTurn({
|
|
1193
1554
|
provider,
|
|
1555
|
+
compactionConfig: compactionConfigFor(options.compaction),
|
|
1556
|
+
...(options.compaction?.consolidate ? { consolidateInto: memoryStore } : {}),
|
|
1557
|
+
...(jobRegistry
|
|
1558
|
+
? {
|
|
1559
|
+
backgroundJobs: jobRegistry,
|
|
1560
|
+
backgroundJobOwner: jobOwner,
|
|
1561
|
+
}
|
|
1562
|
+
: {}),
|
|
1194
1563
|
// Constructed HERE, per turn, and that is not an optimisation to
|
|
1195
1564
|
// undo. `refreshTokenIfNeeded` above replaces the head's client
|
|
1196
1565
|
// object when an OAuth token rotates, so a member list built once at
|
|
@@ -1204,21 +1573,23 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1204
1573
|
pluginManager: pluginRuntime?.manager,
|
|
1205
1574
|
skillRegistry: pluginRuntime?.skills,
|
|
1206
1575
|
skills: pluginSkills,
|
|
1207
|
-
scope,
|
|
1576
|
+
scope: turnScope,
|
|
1208
1577
|
pathBuilder,
|
|
1209
1578
|
workingDirectory: cwd,
|
|
1579
|
+
...(directories.length > 0 ? { additionalDirectories: [...directories] } : {}),
|
|
1580
|
+
...(options.limits ? { limits: options.limits } : {}),
|
|
1210
1581
|
sandboxWorkspace,
|
|
1211
1582
|
rules: options.rules,
|
|
1212
1583
|
reviewAnswer: options.reviewAnswer,
|
|
1213
1584
|
maxAnswerReviews: options.maxAnswerReviews,
|
|
1214
1585
|
promoteMemory,
|
|
1215
|
-
taskStore,
|
|
1586
|
+
taskStore: selectTaskStore(runId, turnScope),
|
|
1216
1587
|
systemPrompt,
|
|
1217
1588
|
messages,
|
|
1218
1589
|
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1219
1590
|
opts: turnOpts,
|
|
1220
1591
|
resumeHandler,
|
|
1221
|
-
taskGateway:
|
|
1592
|
+
taskGateway: await subagentRuntime?.gatewayForRun(runId),
|
|
1222
1593
|
promptContributions,
|
|
1223
1594
|
...(webCapability ? { web: webCapability } : {}),
|
|
1224
1595
|
// Active, not deferred: the doctrine tells the model to open a
|
|
@@ -1249,109 +1620,18 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1249
1620
|
finally {
|
|
1250
1621
|
if (delegatedResumeHandlers.get(runId) === resumeHandler) {
|
|
1251
1622
|
delegatedResumeHandlers.delete(runId);
|
|
1623
|
+
delegationScopes.delete(runId);
|
|
1624
|
+
await subagentRuntime?.releaseRun(runId);
|
|
1252
1625
|
}
|
|
1253
1626
|
}
|
|
1254
1627
|
})()),
|
|
1255
|
-
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) =>
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
await prepareProviderCredential(ownedSignal);
|
|
1261
|
-
const pluginSkills = pluginRuntime
|
|
1262
|
-
? await currentPluginSkills(pluginRuntime.skills)
|
|
1263
|
-
: undefined;
|
|
1264
|
-
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
1265
|
-
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
1266
|
-
const systemPrompt = [
|
|
1267
|
-
NAMZU_IDENTITY,
|
|
1268
|
-
NAMZU_WORKING_DOCTRINE,
|
|
1269
|
-
NAMZU_DELEGATION_DOCTRINE,
|
|
1270
|
-
environmentPrompt,
|
|
1271
|
-
memoryPrompt,
|
|
1272
|
-
]
|
|
1273
|
-
.filter((s) => Boolean(s))
|
|
1274
|
-
.join('\n\n') || undefined;
|
|
1275
|
-
const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
1276
|
-
if (delegatedResumeHandlers.has(entry.runId)) {
|
|
1277
|
-
throw new Error(`Run ${entry.runId} already owns a delegated review channel.`);
|
|
1278
|
-
}
|
|
1279
|
-
delegatedResumeHandlers.set(entry.runId, resumeHandler);
|
|
1280
|
-
try {
|
|
1281
|
-
return await resumeRun({
|
|
1282
|
-
provider,
|
|
1283
|
-
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1284
|
-
tools: registry,
|
|
1285
|
-
pluginManager: pluginRuntime?.manager,
|
|
1286
|
-
skillRegistry: pluginRuntime?.skills,
|
|
1287
|
-
skills: pluginSkills,
|
|
1288
|
-
taskStore,
|
|
1289
|
-
// The same availability the original run registered under.
|
|
1290
|
-
// A resumed run re-registers the task tools; leaving them at
|
|
1291
|
-
// the kernel's `deferred` default would hand the model a plan
|
|
1292
|
-
// it started with active tools and can no longer update.
|
|
1293
|
-
runtimeToolOverrides: {
|
|
1294
|
-
task_create: 'active',
|
|
1295
|
-
task_update: 'active',
|
|
1296
|
-
task_list: 'active',
|
|
1297
|
-
},
|
|
1298
|
-
...(subagentGateway ? { taskGateway: subagentGateway } : {}),
|
|
1299
|
-
authorizationGate: gateFor(options.rules),
|
|
1300
|
-
compactionConfig: COMPACTION_CONFIG,
|
|
1301
|
-
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1302
|
-
pathBuilder,
|
|
1303
|
-
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1304
|
-
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1305
|
-
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
1306
|
-
: {}),
|
|
1307
|
-
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
1308
|
-
// whose `attach` detaches whoever held it before, so a caller
|
|
1309
|
-
// resuming several runs in one process would leave only the last
|
|
1310
|
-
// one covered — and would look covered. A turn owns its process
|
|
1311
|
-
// end to end; a drainer does not.
|
|
1312
|
-
runConfig: {
|
|
1313
|
-
model,
|
|
1314
|
-
...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1315
|
-
timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
1316
|
-
tokenBudget: 1_000_000,
|
|
1317
|
-
maxIterations: 50,
|
|
1318
|
-
maxResponseTokens: 8192,
|
|
1319
|
-
permissionMode: 'auto',
|
|
1320
|
-
},
|
|
1321
|
-
agentId: 'namzu',
|
|
1322
|
-
agentName: 'namzu',
|
|
1323
|
-
...(systemPrompt ? { systemPrompt } : {}),
|
|
1324
|
-
workingDirectory: cwd,
|
|
1325
|
-
// No `onPermission`: there is nobody at a drainer's terminal, so a
|
|
1326
|
-
// prompt would block the pass forever on a run nobody is watching.
|
|
1327
|
-
// The gate's deny rules still apply.
|
|
1328
|
-
// One presenter for the whole stream, built from the registry this
|
|
1329
|
-
// scope already holds. It was the absence of the registry HERE that
|
|
1330
|
-
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
1331
|
-
// over a `RunEvent` and could not ask a tool anything.
|
|
1332
|
-
resumeHandler,
|
|
1333
|
-
signal: ownedSignal,
|
|
1334
|
-
// Attribution comes from the ENTRY, not from this session: the run
|
|
1335
|
-
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
1336
|
-
// it would file another tenant's work under this one.
|
|
1337
|
-
tenantId: entry.tenantId,
|
|
1338
|
-
projectId: entry.projectId,
|
|
1339
|
-
sessionId: entry.sessionId,
|
|
1340
|
-
// …except the topic, which no checkpoint records — see
|
|
1341
|
-
// `RunStateScope`. This one is the drainer's, and honestly so:
|
|
1342
|
-
// supplied here rather than pretended to have been recovered.
|
|
1343
|
-
topicId: scope.topicId,
|
|
1344
|
-
scope: { ...entry, topicId: scope.topicId },
|
|
1345
|
-
checkpointStore,
|
|
1346
|
-
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
finally {
|
|
1350
|
-
if (delegatedResumeHandlers.get(entry.runId) === resumeHandler) {
|
|
1351
|
-
delegatedResumeHandlers.delete(entry.runId);
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1628
|
+
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => kernelResume({
|
|
1629
|
+
entry,
|
|
1630
|
+
checkpointStore,
|
|
1631
|
+
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1632
|
+
...(signal ? { signal } : {}),
|
|
1354
1633
|
}),
|
|
1634
|
+
resumePaused: (params) => resumePausedStream(params),
|
|
1355
1635
|
};
|
|
1356
1636
|
}
|
|
1357
1637
|
function planFallbacks(members, detected) {
|
|
@@ -1631,18 +1911,26 @@ export async function listProviderModels(id, det) {
|
|
|
1631
1911
|
const listing = await describeProviderModels(id, det);
|
|
1632
1912
|
return listing.kind === 'ok' ? [...listing.models] : [];
|
|
1633
1913
|
}
|
|
1634
|
-
/**
|
|
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
|
+
*/
|
|
1635
1928
|
function mintScope() {
|
|
1636
|
-
const suffix = `tui-${Date.now().toString(36)}`;
|
|
1637
|
-
// Through the constructors rather than as four bare template literals.
|
|
1638
|
-
// One suffix shared by four ids is exactly the shape a typo hides in —
|
|
1639
|
-
// `top_` and `tnt_` differ by two characters, and the types accept either
|
|
1640
|
-
// spelling for either field while they are still structural.
|
|
1641
1929
|
return {
|
|
1642
|
-
sessionId:
|
|
1643
|
-
topicId:
|
|
1644
|
-
projectId:
|
|
1645
|
-
tenantId:
|
|
1930
|
+
sessionId: generateSessionId(),
|
|
1931
|
+
topicId: generateTopicId(),
|
|
1932
|
+
projectId: generateProjectId(),
|
|
1933
|
+
tenantId: generateTenantId(),
|
|
1646
1934
|
};
|
|
1647
1935
|
}
|
|
1648
1936
|
// Pre-execution safety gate: hard-deny catastrophic shell patterns
|
|
@@ -1681,7 +1969,7 @@ function gateFor(rules) {
|
|
|
1681
1969
|
// from `runConfig.model`, which is the value the user actually chose. Pinning
|
|
1682
1970
|
// a number here would fix one window across every model the CLI can talk to.
|
|
1683
1971
|
const COMPACTION_CONFIG = {
|
|
1684
|
-
strategy: '
|
|
1972
|
+
strategy: 'salience',
|
|
1685
1973
|
// On, and this is the CLI making a choice rather than taking a default.
|
|
1686
1974
|
// A session's transcript is the only record of what was compacted away;
|
|
1687
1975
|
// the size trade this costs is the operator's to see and turn off.
|
|
@@ -1709,7 +1997,17 @@ const COMPACTION_CONFIG = {
|
|
|
1709
1997
|
maxCharsPerRequirement: 300,
|
|
1710
1998
|
maxCharsPerTask: 400,
|
|
1711
1999
|
};
|
|
1712
|
-
|
|
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, }) {
|
|
1713
2011
|
const signal = opts?.signal;
|
|
1714
2012
|
// One presenter for the whole stream, built from the registry this scope
|
|
1715
2013
|
// already holds. Its absence HERE is what forced presentation to be name
|
|
@@ -1733,7 +2031,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1733
2031
|
// turn. An admitted send owns the exact run-scoped authority above.
|
|
1734
2032
|
...(!opts?.goalRound ? { deniedTools: SESSION_GOAL_TOOL_NAMES } : {}),
|
|
1735
2033
|
taskStore,
|
|
1736
|
-
...(taskGateway ? { taskGateway } : {}),
|
|
2034
|
+
...(taskGateway ? { taskScheduler: taskGateway } : {}),
|
|
1737
2035
|
// `gateFor`, not the bare default: the default's `rules` is a hardcoded
|
|
1738
2036
|
// empty array, so passing it here discarded the operator's rules on the
|
|
1739
2037
|
// path that runs every top-level turn. The sub-agent path called
|
|
@@ -1741,7 +2039,9 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1741
2039
|
...(sandboxProvider ? { sandboxProvider } : {}),
|
|
1742
2040
|
...(sandboxTeardownTimeoutMs !== undefined ? { sandboxTeardownTimeoutMs } : {}),
|
|
1743
2041
|
authorizationGate: gateFor(rules),
|
|
1744
|
-
compactionConfig
|
|
2042
|
+
compactionConfig,
|
|
2043
|
+
...(consolidateInto ? { consolidateInto } : {}),
|
|
2044
|
+
...(backgroundJobs ? { backgroundJobs, backgroundJobOwner } : {}),
|
|
1745
2045
|
// The CLI owns its process end to end, so it can safely hand the
|
|
1746
2046
|
// termination path to the kernel: a Ctrl-C mid-run now leaves a
|
|
1747
2047
|
// dump under the injected hierarchy's emergency partition instead of
|
|
@@ -1752,8 +2052,8 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1752
2052
|
...(sandboxProvider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1753
2053
|
...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
|
|
1754
2054
|
timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
1755
|
-
tokenBudget: 1_000_000,
|
|
1756
|
-
maxIterations: 50,
|
|
2055
|
+
tokenBudget: limits?.tokenBudget ?? 1_000_000,
|
|
2056
|
+
maxIterations: limits?.maxIterations ?? 50,
|
|
1757
2057
|
maxResponseTokens: 8192,
|
|
1758
2058
|
permissionMode: 'auto',
|
|
1759
2059
|
},
|
|
@@ -1774,6 +2074,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1774
2074
|
messages: [...messages],
|
|
1775
2075
|
...(opts?.inboundMessages ? { inboundMessages: opts.inboundMessages } : {}),
|
|
1776
2076
|
workingDirectory,
|
|
2077
|
+
...(additionalDirectories?.length ? { additionalDirectories } : {}),
|
|
1777
2078
|
// The exemption reads `tools` at decision time, so it sees the task
|
|
1778
2079
|
// tools `query()` registers deferred below and any tool server that
|
|
1779
2080
|
// connected after this session was built.
|
|
@@ -1830,143 +2131,26 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1830
2131
|
}
|
|
1831
2132
|
}
|
|
1832
2133
|
/**
|
|
1833
|
-
*
|
|
1834
|
-
* callback. Read-only batches (nothing destructive) run silently; batches
|
|
1835
|
-
* with a destructive call prompt the user unless they've already chosen
|
|
1836
|
-
* "approve all" for the session. Plans and iteration checkpoints are
|
|
1837
|
-
* auto-continued (the TUI doesn't use plan mode).
|
|
1838
|
-
*/
|
|
1839
|
-
export function makeResumeHandler(approval, onPermission, mode = onPermission ? 'prompt' : 'auto',
|
|
1840
|
-
/**
|
|
1841
|
-
* Which calls skip the prompt. Injected rather than reached for, so this
|
|
1842
|
-
* handler stays testable without a registry — and so the answer comes from
|
|
1843
|
-
* the live roster at the moment of the call.
|
|
1844
|
-
*/
|
|
1845
|
-
exempt = () => false) {
|
|
1846
|
-
return async (request) => {
|
|
1847
|
-
if (request.type !== 'tool_review') {
|
|
1848
|
-
return request.type === 'plan_approval' ? { action: 'approve_plan' } : { action: 'continue' };
|
|
1849
|
-
}
|
|
1850
|
-
// Only calls the gate routed to REVIEW arrive here — a rule that denied
|
|
1851
|
-
// one already stopped it, and a rule that allowed one never asked. So the
|
|
1852
|
-
// mode decides what happens to the undecided, and cannot reopen anything
|
|
1853
|
-
// a rule closed. That is the whole precedence story between a flag and a
|
|
1854
|
-
// config file, and it is one sentence on purpose.
|
|
1855
|
-
if (!batchNeedsPrompt(request.toolCalls, exempt)) {
|
|
1856
|
-
return { action: 'approve_tools' };
|
|
1857
|
-
}
|
|
1858
|
-
// A batch of nothing but non-destructive file edits is the case this
|
|
1859
|
-
// mode exists for. One bash call in the same batch and the whole batch
|
|
1860
|
-
// asks — the operator reviews the batch as a unit, and a prompt that
|
|
1861
|
-
// showed only the shell command while the edits went through beside it
|
|
1862
|
-
// would be approving something it did not show.
|
|
1863
|
-
if (mode === 'accept-edits' &&
|
|
1864
|
-
request.toolCalls.every((tc) => !tc.isDestructive && (ACCEPT_EDITS_TOOLS.has(tc.name) || exempt(tc.name, tc.input)))) {
|
|
1865
|
-
return { action: 'approve_tools' };
|
|
1866
|
-
}
|
|
1867
|
-
// Reads were already approved above (they are exempt). Anything that
|
|
1868
|
-
// reached here would change something, and plan mode's answer to that
|
|
1869
|
-
// is the same every time: not now, tell the user what you would do.
|
|
1870
|
-
if (mode === 'plan') {
|
|
1871
|
-
return { action: 'reject_tools', feedback: PLAN_MODE_REFUSAL };
|
|
1872
|
-
}
|
|
1873
|
-
if (mode === 'strict') {
|
|
1874
|
-
return {
|
|
1875
|
-
action: 'reject_tools',
|
|
1876
|
-
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.',
|
|
1877
|
-
};
|
|
1878
|
-
}
|
|
1879
|
-
if (mode === 'auto' || !onPermission || approval.all) {
|
|
1880
|
-
return { action: 'approve_tools' };
|
|
1881
|
-
}
|
|
1882
|
-
const decision = await onPermission({
|
|
1883
|
-
toolCalls: request.toolCalls.map((tc) => ({
|
|
1884
|
-
id: tc.id,
|
|
1885
|
-
name: tc.name,
|
|
1886
|
-
input: tc.input,
|
|
1887
|
-
isDestructive: tc.isDestructive,
|
|
1888
|
-
})),
|
|
1889
|
-
});
|
|
1890
|
-
switch (decision.kind) {
|
|
1891
|
-
case 'approve':
|
|
1892
|
-
return { action: 'approve_tools' };
|
|
1893
|
-
case 'approve-all':
|
|
1894
|
-
approval.all = true;
|
|
1895
|
-
return { action: 'approve_tools' };
|
|
1896
|
-
case 'reject':
|
|
1897
|
-
return {
|
|
1898
|
-
action: 'reject_tools',
|
|
1899
|
-
feedback: decision.feedback ?? 'User declined to run the proposed tool(s).',
|
|
1900
|
-
};
|
|
1901
|
-
}
|
|
1902
|
-
};
|
|
1903
|
-
}
|
|
1904
|
-
/**
|
|
1905
|
-
* Writes that skip the prompt anyway, in spite of declaring `readOnly: false`.
|
|
1906
|
-
*
|
|
1907
|
-
* This is an OVERRIDE of the tool's own declaration, and it is named as one.
|
|
1908
|
-
* The list it replaced was called `READ_ONLY_TOOLS` and contained three tools
|
|
1909
|
-
* that declare `readOnly: false` — a constant asserting the exact property it
|
|
1910
|
-
* was getting wrong, which is how the disagreement survived: nothing reading it
|
|
1911
|
-
* had reason to doubt the name.
|
|
1912
|
-
*
|
|
1913
|
-
* The bar for an entry is that prompting would be unusable AND a bad write
|
|
1914
|
-
* cannot reach beyond the agent's own bookkeeping. Each one is justified here,
|
|
1915
|
-
* or it does not belong here.
|
|
2134
|
+
* The kernel's review policy with the TUI's prompt behind it.
|
|
1916
2135
|
*
|
|
1917
|
-
*
|
|
1918
|
-
*
|
|
1919
|
-
*
|
|
1920
|
-
* costs is a polluted task list, which is visible in the transcript and
|
|
1921
|
-
* grants nothing. Worth knowing while reading that: these DO outlive the
|
|
1922
|
-
* session, because the CLI's task store uses a fixed run id
|
|
1923
|
-
* (`run_namzu-cli`), so "run-scoped" is not the reason they are here — the
|
|
1924
|
-
* blast radius is.
|
|
1925
|
-
*
|
|
1926
|
-
* `save_memory` was on the list it replaced and is deliberately NOT here. Its
|
|
1927
|
-
* effect outlives the run in a way the task tools' does not: content saved now
|
|
1928
|
-
* is retrievable by `search_memory` in a later session, so a tool result or
|
|
1929
|
-
* fetched page that talks the model into saving something reaches a future
|
|
1930
|
-
* run's reasoning. It is not auto-injected into the prompt — that is
|
|
1931
|
-
* `MEMORY.md`, a different thing — but retrievable is enough. A write that
|
|
1932
|
-
* survives the process, into the user's own repository, is not read-only under
|
|
1933
|
-
* 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.
|
|
1934
2139
|
*/
|
|
1935
|
-
|
|
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
|
+
}
|
|
1936
2148
|
/**
|
|
1937
|
-
* Whether a call runs without asking:
|
|
1938
|
-
* named
|
|
1939
|
-
*
|
|
1940
|
-
* The read-only half comes from the tool's own `isReadOnly(input)`, never from
|
|
1941
|
-
* a list of names kept here. A name list in the consumer is a second source of
|
|
1942
|
-
* truth for a property the producer already states: a new read-only tool
|
|
1943
|
-
* missing from it merely gets prompted, but a RENAMED tool silently changes
|
|
1944
|
-
* posture with nothing to notice.
|
|
1945
|
-
*
|
|
1946
|
-
* Resolved per call rather than snapshotted, because the roster changes after
|
|
1947
|
-
* this module has run — the task tools are registered deferred inside
|
|
1948
|
-
* `query()`, and tool servers connect during startup, so anything computed
|
|
1949
|
-
* eagerly would be answering about a registry that no longer exists.
|
|
1950
|
-
*
|
|
1951
|
-
* A tool the registry does not know, or one that declares nothing, prompts.
|
|
1952
|
-
* That is the safe-by-default direction the previous comment claimed and this
|
|
1953
|
-
* 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.
|
|
1954
2152
|
*/
|
|
1955
|
-
export
|
|
1956
|
-
if (PROMPT_EXEMPT_WRITES.has(name.toLowerCase()))
|
|
1957
|
-
return true;
|
|
1958
|
-
const tool = registry.get(name) ?? registry.get(name.toLowerCase());
|
|
1959
|
-
// A fetch changes nothing here and declares itself read-only, and it is
|
|
1960
|
-
// still a request leaving the machine to an address the model chose. The
|
|
1961
|
-
// operator sees the URL before it goes, the way they see a shell command.
|
|
1962
|
-
if (tool?.category === 'network')
|
|
1963
|
-
return false;
|
|
1964
|
-
// A connected server's own claim about its own tool cannot skip the
|
|
1965
|
-
// prompt. Same predicate the kernel gate and plan mode use -- three
|
|
1966
|
-
// doors, one rule, because fixing two would close the issue and leave
|
|
1967
|
-
// the boundary open.
|
|
1968
|
-
return isTrustedReadOnly(tool, input);
|
|
1969
|
-
}
|
|
2153
|
+
export const isPromptExempt = isReviewExempt;
|
|
1970
2154
|
/** The exempt roster, sorted, for the surface that has to NAME it. */
|
|
1971
2155
|
export function promptExemptToolNames(registry) {
|
|
1972
2156
|
return registry
|
|
@@ -1975,13 +2159,8 @@ export function promptExemptToolNames(registry) {
|
|
|
1975
2159
|
.map((t) => t.name)
|
|
1976
2160
|
.sort();
|
|
1977
2161
|
}
|
|
1978
|
-
/**
|
|
1979
|
-
|
|
1980
|
-
* destructive by the SDK, or not exempt from the prompt.
|
|
1981
|
-
*/
|
|
1982
|
-
export function batchNeedsPrompt(toolCalls, exempt) {
|
|
1983
|
-
return toolCalls.some((tc) => tc.isDestructive || !exempt(tc.name, tc.input));
|
|
1984
|
-
}
|
|
2162
|
+
/** A batch needs explicit approval when any call mutates state. */
|
|
2163
|
+
export const batchNeedsPrompt = batchNeedsReview;
|
|
1985
2164
|
/**
|
|
1986
2165
|
* Translate one SDK `RunEvent` into the TUI's `AgentEvent` vocabulary, or
|
|
1987
2166
|
* `null` for events the chat surface doesn't render (iteration markers,
|
|
@@ -1996,6 +2175,14 @@ export function toAgentEvent(event, presenter) {
|
|
|
1996
2175
|
...(event.messageId ? { messageId: event.messageId } : {}),
|
|
1997
2176
|
...(event.runId ? { runId: event.runId } : {}),
|
|
1998
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 };
|
|
1999
2186
|
case 'tool_executing':
|
|
2000
2187
|
return {
|
|
2001
2188
|
kind: 'tool-start',
|
|
@@ -2027,9 +2214,15 @@ export function toAgentEvent(event, presenter) {
|
|
|
2027
2214
|
success: !event.isError,
|
|
2028
2215
|
output: event.result,
|
|
2029
2216
|
});
|
|
2030
|
-
const summary = firstLine(event.result);
|
|
2031
2217
|
const detail = viewToLines(view);
|
|
2032
|
-
|
|
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;
|
|
2033
2226
|
return {
|
|
2034
2227
|
kind: 'tool-end',
|
|
2035
2228
|
runId: event.runId,
|
|
@@ -2061,6 +2254,7 @@ export function toAgentEvent(event, presenter) {
|
|
|
2061
2254
|
return {
|
|
2062
2255
|
kind: 'usage',
|
|
2063
2256
|
totalTokens: event.usage.totalTokens,
|
|
2257
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2064
2258
|
cost: event.cost,
|
|
2065
2259
|
...(event.contextTokens !== undefined ? { contextTokens: event.contextTokens } : {}),
|
|
2066
2260
|
...(event.contextMeasuredBy !== undefined
|
|
@@ -2124,6 +2318,8 @@ export function toAgentEvent(event, presenter) {
|
|
|
2124
2318
|
// the SDK had explicitly stopped.
|
|
2125
2319
|
return {
|
|
2126
2320
|
kind: 'paused',
|
|
2321
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2322
|
+
runId: String(event.runId),
|
|
2127
2323
|
checkpointId: event.checkpointId,
|
|
2128
2324
|
reason: event.reason,
|
|
2129
2325
|
...(event.failure ? { failure: event.failure } : {}),
|
|
@@ -2138,6 +2334,7 @@ export function toAgentEvent(event, presenter) {
|
|
|
2138
2334
|
// answer was refused.
|
|
2139
2335
|
return {
|
|
2140
2336
|
kind: 'done',
|
|
2337
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2141
2338
|
...(event.stopReason ? { stopReason: event.stopReason } : {}),
|
|
2142
2339
|
};
|
|
2143
2340
|
case 'run_failed':
|
|
@@ -2147,13 +2344,29 @@ export function toAgentEvent(event, presenter) {
|
|
|
2147
2344
|
// still forcing every host to parse prose.
|
|
2148
2345
|
return {
|
|
2149
2346
|
kind: 'error',
|
|
2347
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2150
2348
|
message: event.error,
|
|
2151
2349
|
...(event.failure ? { failure: event.failure } : {}),
|
|
2152
2350
|
...(event.providerError ? { providerError: event.providerError } : {}),
|
|
2153
2351
|
...(event.explanation ? { explanation: event.explanation } : {}),
|
|
2154
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
|
+
};
|
|
2155
2362
|
case 'compaction_completed':
|
|
2156
|
-
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
|
+
};
|
|
2157
2370
|
case 'compaction_tool_results_cleared':
|
|
2158
2371
|
// `shed: true` on both branches: the tool-result bodies are gone
|
|
2159
2372
|
// either way. `reliefWasEnough: false` additionally means a
|
|
@@ -2161,8 +2374,11 @@ export function toAgentEvent(event, presenter) {
|
|
|
2161
2374
|
// so this one says what IT cost rather than claiming the total.
|
|
2162
2375
|
return {
|
|
2163
2376
|
kind: 'context',
|
|
2164
|
-
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'}`,
|
|
2165
2378
|
shed: true,
|
|
2379
|
+
cleared: event.clearedCount,
|
|
2380
|
+
stubbed: event.stubbedCount ?? 0,
|
|
2381
|
+
reclaimedTokens: event.reclaimedTokens,
|
|
2166
2382
|
};
|
|
2167
2383
|
case 'compaction_failed':
|
|
2168
2384
|
return {
|
|
@@ -2502,6 +2718,9 @@ function emptySession(errorHint, errorKind = 'environment') {
|
|
|
2502
2718
|
resumeDurable: async () => {
|
|
2503
2719
|
throw new Error(errorHint);
|
|
2504
2720
|
},
|
|
2721
|
+
resumePaused: async function* () {
|
|
2722
|
+
yield { kind: 'error', message: 'no provider: nothing to resume' };
|
|
2723
|
+
},
|
|
2505
2724
|
close: async () => {
|
|
2506
2725
|
// Nothing was ever connected on this path.
|
|
2507
2726
|
},
|