@namzu/cli 18.1.0 → 20.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5951 -0
- package/README.md +23 -24
- package/dist/bin.js +12 -1
- package/dist/bin.js.map +1 -1
- package/dist/checkpoints/store.d.ts +75 -0
- package/dist/checkpoints/store.d.ts.map +1 -0
- package/dist/checkpoints/store.js +177 -0
- package/dist/checkpoints/store.js.map +1 -0
- package/dist/checkpoints/wrap.d.ts +15 -0
- package/dist/checkpoints/wrap.d.ts.map +1 -0
- package/dist/checkpoints/wrap.js +37 -0
- package/dist/checkpoints/wrap.js.map +1 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/acp.d.ts.map +1 -1
- package/dist/commands/acp.js +2 -0
- package/dist/commands/acp.js.map +1 -1
- package/dist/commands/drain.d.ts.map +1 -1
- package/dist/commands/drain.js +15 -2
- package/dist/commands/drain.js.map +1 -1
- package/dist/commands/provider-wait.d.ts +44 -0
- package/dist/commands/provider-wait.d.ts.map +1 -0
- package/dist/commands/provider-wait.js +65 -0
- package/dist/commands/provider-wait.js.map +1 -0
- package/dist/commands/run-flags.d.ts +13 -1
- package/dist/commands/run-flags.d.ts.map +1 -1
- package/dist/commands/run-flags.js +41 -0
- package/dist/commands/run-flags.js.map +1 -1
- package/dist/commands/run-stream.d.ts.map +1 -1
- package/dist/commands/run-stream.js +33 -13
- package/dist/commands/run-stream.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +132 -78
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/stdin.d.ts +17 -0
- package/dist/commands/stdin.d.ts.map +1 -0
- package/dist/commands/stdin.js +69 -0
- package/dist/commands/stdin.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +107 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +80 -5
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/doctrine.d.ts +16 -38
- package/dist/context/doctrine.d.ts.map +1 -1
- package/dist/context/doctrine.js +16 -75
- package/dist/context/doctrine.js.map +1 -1
- package/dist/context/environment.d.ts +2 -0
- package/dist/context/environment.d.ts.map +1 -1
- package/dist/context/environment.js +3 -0
- package/dist/context/environment.js.map +1 -1
- package/dist/integrations/mcp/servers.d.ts +19 -0
- package/dist/integrations/mcp/servers.d.ts.map +1 -1
- package/dist/integrations/mcp/servers.js +23 -2
- package/dist/integrations/mcp/servers.js.map +1 -1
- package/dist/integrations/plugins/runtime.d.ts +8 -2
- package/dist/integrations/plugins/runtime.d.ts.map +1 -1
- package/dist/integrations/plugins/runtime.js +24 -12
- package/dist/integrations/plugins/runtime.js.map +1 -1
- package/dist/integrations/providers/chain-capabilities.js +1 -1
- package/dist/integrations/providers/chain-capabilities.js.map +1 -1
- package/dist/integrations/providers/credential-store.d.ts +1 -1
- package/dist/integrations/providers/credential-store.js +1 -1
- package/dist/integrations/providers/harness-credentials.d.ts.map +1 -1
- package/dist/integrations/providers/harness-credentials.js +7 -0
- package/dist/integrations/providers/harness-credentials.js.map +1 -1
- package/dist/integrations/providers/registry.d.ts +2 -2
- package/dist/integrations/providers/registry.js +2 -2
- package/dist/integrations/sessions/store.d.ts +14 -22
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +78 -175
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/sessions/transcript-export.d.ts.map +1 -1
- package/dist/integrations/sessions/transcript-export.js +6 -2
- package/dist/integrations/sessions/transcript-export.js.map +1 -1
- package/dist/integrations/state/identity.d.ts +21 -0
- package/dist/integrations/state/identity.d.ts.map +1 -0
- package/dist/integrations/state/identity.js +49 -0
- package/dist/integrations/state/identity.js.map +1 -0
- package/dist/integrations/state/immutable-json.d.ts +10 -0
- package/dist/integrations/state/immutable-json.d.ts.map +1 -0
- package/dist/integrations/state/immutable-json.js +38 -0
- package/dist/integrations/state/immutable-json.js.map +1 -0
- package/dist/integrations/state/project.d.ts +10 -0
- package/dist/integrations/state/project.d.ts.map +1 -0
- package/dist/integrations/state/project.js +15 -0
- package/dist/integrations/state/project.js.map +1 -0
- package/dist/integrations/state/report.d.ts.map +1 -1
- package/dist/integrations/state/report.js +42 -12
- package/dist/integrations/state/report.js.map +1 -1
- package/dist/integrations/subagents/definitions.d.ts +19 -0
- package/dist/integrations/subagents/definitions.d.ts.map +1 -0
- package/dist/integrations/subagents/definitions.js +26 -0
- package/dist/integrations/subagents/definitions.js.map +1 -0
- package/dist/integrations/subagents/parent.d.ts +13 -0
- package/dist/integrations/subagents/parent.d.ts.map +1 -0
- package/dist/integrations/subagents/parent.js +57 -0
- package/dist/integrations/subagents/parent.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +33 -16
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +450 -150
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/memory/presentation.d.ts +9 -0
- package/dist/memory/presentation.d.ts.map +1 -0
- package/dist/memory/presentation.js +37 -0
- package/dist/memory/presentation.js.map +1 -0
- package/dist/memory/store.d.ts +35 -15
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js +79 -25
- package/dist/memory/store.js.map +1 -1
- package/dist/permissions/mode.d.ts +15 -45
- package/dist/permissions/mode.d.ts.map +1 -1
- package/dist/permissions/mode.js +36 -13
- package/dist/permissions/mode.js.map +1 -1
- package/dist/release-notes.d.ts +20 -0
- package/dist/release-notes.d.ts.map +1 -0
- package/dist/release-notes.js +64 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/tui/AgentExplorer.d.ts +1 -1
- package/dist/tui/AgentExplorer.d.ts.map +1 -1
- package/dist/tui/AgentExplorer.js +25 -21
- package/dist/tui/AgentExplorer.js.map +1 -1
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +701 -245
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/BrandHeader.d.ts +12 -0
- package/dist/tui/BrandHeader.d.ts.map +1 -0
- package/dist/tui/BrandHeader.js +18 -0
- package/dist/tui/BrandHeader.js.map +1 -0
- package/dist/tui/ChoicePicker.d.ts +11 -1
- package/dist/tui/ChoicePicker.d.ts.map +1 -1
- package/dist/tui/ChoicePicker.js +46 -13
- package/dist/tui/ChoicePicker.js.map +1 -1
- package/dist/tui/Composer.d.ts +5 -2
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +6 -11
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/ComposerFrame.d.ts +10 -0
- package/dist/tui/ComposerFrame.d.ts.map +1 -0
- package/dist/tui/ComposerFrame.js +93 -0
- package/dist/tui/ComposerFrame.js.map +1 -0
- package/dist/tui/EditPromptPicker.d.ts.map +1 -1
- package/dist/tui/EditPromptPicker.js +1 -1
- package/dist/tui/EditPromptPicker.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +12 -5
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +17 -32
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +26 -2
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +109 -16
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/Picker.d.ts +2 -2
- package/dist/tui/Picker.d.ts.map +1 -1
- package/dist/tui/Picker.js +82 -52
- package/dist/tui/Picker.js.map +1 -1
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +5 -2
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/TaskList.d.ts +3 -1
- package/dist/tui/TaskList.d.ts.map +1 -1
- package/dist/tui/TaskList.js +13 -4
- package/dist/tui/TaskList.js.map +1 -1
- package/dist/tui/Transcript.d.ts +4 -12
- package/dist/tui/Transcript.d.ts.map +1 -1
- package/dist/tui/Transcript.js +45 -75
- package/dist/tui/Transcript.js.map +1 -1
- package/dist/tui/agent.d.ts +116 -51
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +539 -307
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/choice-selection.d.ts +17 -0
- package/dist/tui/choice-selection.d.ts.map +1 -0
- package/dist/tui/choice-selection.js +38 -0
- package/dist/tui/choice-selection.js.map +1 -0
- package/dist/tui/exit-summary.d.ts +7 -1
- package/dist/tui/exit-summary.d.ts.map +1 -1
- package/dist/tui/exit-summary.js +12 -3
- package/dist/tui/exit-summary.js.map +1 -1
- package/dist/tui/index.d.ts +3 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +5 -3
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/logo.d.ts +7 -22
- package/dist/tui/logo.d.ts.map +1 -1
- package/dist/tui/logo.js +8 -34
- package/dist/tui/logo.js.map +1 -1
- package/dist/tui/model-choices.d.ts.map +1 -1
- package/dist/tui/model-choices.js +21 -6
- package/dist/tui/model-choices.js.map +1 -1
- package/dist/tui/permission-review.d.ts +15 -4
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +48 -9
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/provider-selection.d.ts +4 -0
- package/dist/tui/provider-selection.d.ts.map +1 -0
- package/dist/tui/provider-selection.js +16 -0
- package/dist/tui/provider-selection.js.map +1 -0
- package/dist/tui/run-interruption.d.ts +3 -2
- package/dist/tui/run-interruption.d.ts.map +1 -1
- package/dist/tui/run-interruption.js +2 -1
- package/dist/tui/run-interruption.js.map +1 -1
- package/dist/tui/shell-escape.d.ts +42 -0
- package/dist/tui/shell-escape.d.ts.map +1 -0
- package/dist/tui/shell-escape.js +110 -0
- package/dist/tui/shell-escape.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +82 -31
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +518 -306
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/terminal-choice-text.d.ts +8 -0
- package/dist/tui/terminal-choice-text.d.ts.map +1 -0
- package/dist/tui/terminal-choice-text.js +37 -0
- package/dist/tui/terminal-choice-text.js.map +1 -0
- package/dist/tui/theme.d.ts +3 -4
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +16 -17
- package/dist/tui/theme.js.map +1 -1
- package/dist/tui/types.d.ts +9 -13
- package/dist/tui/types.d.ts.map +1 -1
- package/package.json +7 -5
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,8 @@ 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();
|
|
893
|
+
const delegatedInputWaiters = new Map();
|
|
808
894
|
let subagentRuntime;
|
|
809
895
|
// Stays empty when the runtime below throws, which is the honest answer: the
|
|
810
896
|
// catch is non-fatal and the session then genuinely has no delegate to
|
|
@@ -812,12 +898,31 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
812
898
|
// would name agents that are not there.
|
|
813
899
|
let allowedAgentIds = [];
|
|
814
900
|
try {
|
|
901
|
+
// Agents the project or user defined in files. A file that cannot be
|
|
902
|
+
// loaded is named with its reason rather than silently absent: "namzu
|
|
903
|
+
// ignored my reviewer" and "namzu never saw it" call for opposite fixes.
|
|
904
|
+
const discovered = await discoverAgentDefinitions({ cwd });
|
|
905
|
+
for (const skipped of discovered.skipped) {
|
|
906
|
+
cliLogger().warn('agent definition skipped', {
|
|
907
|
+
'namzu.agent.definition.path': skipped.path,
|
|
908
|
+
'namzu.agent.definition.reason': skipped.reason,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
815
911
|
const sub = await createSubagentRuntime({
|
|
816
912
|
cwd,
|
|
817
913
|
model,
|
|
818
|
-
|
|
914
|
+
tokenBudget: options.limits?.tokenBudget,
|
|
915
|
+
definitions: discovered.definitions,
|
|
916
|
+
pathBuilder: new SubagentPathBuilder(projectStateRoot, scope.projectId),
|
|
917
|
+
resolveParent: async (runId) => {
|
|
918
|
+
const parent = delegationScopes.get(runId);
|
|
919
|
+
if (!parent)
|
|
920
|
+
throw new Error(`Run ${runId} no longer owns delegation authority`);
|
|
921
|
+
return resolveSubagentParent(parent, cwd, options.stateRoot ? hierarchyRoot : undefined);
|
|
922
|
+
},
|
|
819
923
|
sandboxWorkspace,
|
|
820
924
|
resolveResumeHandler: (runId) => delegatedResumeHandlers.get(runId),
|
|
925
|
+
resolveWaitForInbound: (runId) => delegatedInputWaiters.get(runId),
|
|
821
926
|
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
822
927
|
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
823
928
|
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
@@ -861,8 +966,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
861
966
|
authorizationGate: gateFor(options.rules),
|
|
862
967
|
});
|
|
863
968
|
subagentRuntime = sub;
|
|
864
|
-
registry.register([sub.agentTool]);
|
|
865
|
-
subagentGateway = sub.gateway;
|
|
969
|
+
registry.register([sub.agentTool, sub.waitForTaskTool]);
|
|
866
970
|
allowedAgentIds = sub.allowedAgentIds;
|
|
867
971
|
}
|
|
868
972
|
catch (err) {
|
|
@@ -883,7 +987,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
883
987
|
// gateway the tool builder requires; a session without one has no
|
|
884
988
|
// question tool either, and says nothing — it also has no `Agent`.
|
|
885
989
|
let currentOnQuestion;
|
|
886
|
-
if (options.askUser &&
|
|
990
|
+
if (options.askUser && subagentRuntime) {
|
|
887
991
|
const parkQuestion = async (request) => {
|
|
888
992
|
if (request.type !== 'user_question')
|
|
889
993
|
return { action: 'continue' };
|
|
@@ -905,20 +1009,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
905
1009
|
return { action: 'continue' };
|
|
906
1010
|
}
|
|
907
1011
|
};
|
|
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);
|
|
1012
|
+
// The park request carries the run id of the call that asked; the
|
|
1013
|
+
// handler above routes by the question, not by the run, and no durable
|
|
1014
|
+
// park recorder is supplied.
|
|
1015
|
+
registry.register(buildAskUserQuestionTool({ resumeHandler: parkQuestion }));
|
|
922
1016
|
}
|
|
923
1017
|
// Task store → query registers task_create / task_update / task_list and
|
|
924
1018
|
// emits task_created/task_updated, so the agent can track a plan for the
|
|
@@ -937,11 +1031,39 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
937
1031
|
// captured on this line. The count at connect time is unchanged; what
|
|
938
1032
|
// changes is that asking again later gets a later answer.
|
|
939
1033
|
ensurePrivateStateDirectory(projectStateRoot, 'tenants');
|
|
940
|
-
const
|
|
1034
|
+
const taskStoreForRun = (runId, tenantId) => new DiskTaskStore({
|
|
941
1035
|
baseDir: projectStateRoot,
|
|
942
|
-
defaultRunId:
|
|
943
|
-
tenantId
|
|
1036
|
+
defaultRunId: runId,
|
|
1037
|
+
tenantId,
|
|
944
1038
|
});
|
|
1039
|
+
let selectedTaskStore;
|
|
1040
|
+
let taskSelectionGeneration = 0;
|
|
1041
|
+
const resetTaskStore = () => {
|
|
1042
|
+
selectedTaskStore = undefined;
|
|
1043
|
+
taskSelectionGeneration += 1;
|
|
1044
|
+
};
|
|
1045
|
+
const matchesCurrentScope = (candidate) => candidate.sessionId === scope.sessionId &&
|
|
1046
|
+
candidate.projectId === scope.projectId &&
|
|
1047
|
+
candidate.tenantId === scope.tenantId &&
|
|
1048
|
+
candidate.topicId === scope.topicId;
|
|
1049
|
+
const currentTaskStore = () => {
|
|
1050
|
+
if (selectedTaskStore && !matchesCurrentScope(selectedTaskStore.scope))
|
|
1051
|
+
resetTaskStore();
|
|
1052
|
+
return selectedTaskStore?.store;
|
|
1053
|
+
};
|
|
1054
|
+
const beginTaskStoreReadout = () => {
|
|
1055
|
+
// A starting turn must not show its predecessor's plan while credentials
|
|
1056
|
+
// and other asynchronous setup are still being prepared.
|
|
1057
|
+
resetTaskStore();
|
|
1058
|
+
const generation = taskSelectionGeneration;
|
|
1059
|
+
return (runId, runScope) => {
|
|
1060
|
+
const store = taskStoreForRun(runId, runScope.tenantId);
|
|
1061
|
+
if (generation === taskSelectionGeneration && matchesCurrentScope(runScope)) {
|
|
1062
|
+
selectedTaskStore = { scope: { ...runScope }, store };
|
|
1063
|
+
}
|
|
1064
|
+
return store;
|
|
1065
|
+
};
|
|
1066
|
+
};
|
|
945
1067
|
// Persists across turns: once the user picks "approve all", later tool
|
|
946
1068
|
// batches in this session run without prompting.
|
|
947
1069
|
const approval = { all: false };
|
|
@@ -963,12 +1085,31 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
963
1085
|
// registries, so executable plugins remain a top-level-session capability.
|
|
964
1086
|
let pluginRuntime;
|
|
965
1087
|
try {
|
|
966
|
-
pluginRuntime = await createCliPluginRuntime(options.plugins, registry, cwd);
|
|
1088
|
+
pluginRuntime = await createCliPluginRuntime(options.plugins, registry, cwd, options.hooks);
|
|
967
1089
|
}
|
|
968
1090
|
catch (error) {
|
|
969
1091
|
await Promise.allSettled([mcp.close(), computerUseHost?.dispose()]);
|
|
970
1092
|
return emptySession(describeError(error));
|
|
971
1093
|
}
|
|
1094
|
+
// The session's own lifecycle, for hooks that set up or tear down
|
|
1095
|
+
// something per session rather than per run. The run id is minted for
|
|
1096
|
+
// these two calls: they belong to no turn. `session_start` waits for
|
|
1097
|
+
// the first turn rather than firing here, because the conversation id
|
|
1098
|
+
// the scope holds at construction is provisional — it is replaced when
|
|
1099
|
+
// the conversation is first made durable — and a hook given the
|
|
1100
|
+
// provisional id could never match it to a run.
|
|
1101
|
+
const sessionPlugins = pluginRuntime;
|
|
1102
|
+
const sessionHookRunId = generateRunId();
|
|
1103
|
+
let sessionStarted = false;
|
|
1104
|
+
const announceSessionStart = async () => {
|
|
1105
|
+
if (!sessionPlugins || sessionStarted)
|
|
1106
|
+
return;
|
|
1107
|
+
sessionStarted = true;
|
|
1108
|
+
await sessionPlugins.manager.executeHooks('session_start', {
|
|
1109
|
+
runId: sessionHookRunId,
|
|
1110
|
+
sessionId: scope.sessionId,
|
|
1111
|
+
});
|
|
1112
|
+
};
|
|
972
1113
|
if (pluginRuntime) {
|
|
973
1114
|
cliLogger().info('discovery complete', {
|
|
974
1115
|
[EVENT_NAME_ATTRIBUTE]: BOOT_EVENT_NAMES.DISCOVERY_COMPLETED,
|
|
@@ -990,9 +1131,19 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
990
1131
|
const operations = new SessionOperationOwner(async () => {
|
|
991
1132
|
const results = await Promise.allSettled([
|
|
992
1133
|
subagentRuntime?.close?.(),
|
|
993
|
-
|
|
1134
|
+
sessionPlugins
|
|
1135
|
+
? sessionPlugins.manager
|
|
1136
|
+
.executeHooks('session_end', {
|
|
1137
|
+
runId: sessionHookRunId,
|
|
1138
|
+
sessionId: scope.sessionId,
|
|
1139
|
+
})
|
|
1140
|
+
.catch(() => [])
|
|
1141
|
+
.then(() => sessionPlugins.close())
|
|
1142
|
+
: undefined,
|
|
994
1143
|
mcp.close(),
|
|
995
1144
|
computerUseHost?.dispose(),
|
|
1145
|
+
jobRegistry?.killOwner(jobOwner),
|
|
1146
|
+
checkpoints.close(),
|
|
996
1147
|
]);
|
|
997
1148
|
const failures = results
|
|
998
1149
|
.filter((result) => result.status === 'rejected')
|
|
@@ -1032,6 +1183,200 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1032
1183
|
reasoningEffortDefault = undefined;
|
|
1033
1184
|
effortNotice = `Reasoning effort levels could not be established for this session: ${describeError(error)}`;
|
|
1034
1185
|
}
|
|
1186
|
+
/**
|
|
1187
|
+
* The kernel's resume with this session's half of the run attached: the
|
|
1188
|
+
* provider, the tools, the working directory, the doctrine — the part a
|
|
1189
|
+
* checkpoint cannot carry. `resumeDurable` and `resumePaused` differ only
|
|
1190
|
+
* in where the run and its store come from.
|
|
1191
|
+
*/
|
|
1192
|
+
const kernelResume = ({ entry, checkpointStore, claimFence, signal, checkpointId, listener, }) => operations.promise(signal, async (ownedSignal) => {
|
|
1193
|
+
const selectTaskStore = beginTaskStoreReadout();
|
|
1194
|
+
// The same prelude a turn runs, and for the same reasons: a lapsed
|
|
1195
|
+
// OAuth token has to be renewed before the provider is used, and the
|
|
1196
|
+
// fallback chain has to be built AFTER that so its members do not
|
|
1197
|
+
// hold a client the refresh just replaced.
|
|
1198
|
+
await prepareProviderCredential(ownedSignal);
|
|
1199
|
+
const pluginSkills = pluginRuntime
|
|
1200
|
+
? await currentPluginSkills(pluginRuntime.skills)
|
|
1201
|
+
: undefined;
|
|
1202
|
+
const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
|
|
1203
|
+
const environmentPrompt = composeEnvironmentPrompt({
|
|
1204
|
+
...(await readEnvironmentFacts(cwd)),
|
|
1205
|
+
additionalDirectories: [...directories],
|
|
1206
|
+
});
|
|
1207
|
+
const systemPrompt = [
|
|
1208
|
+
NAMZU_IDENTITY,
|
|
1209
|
+
NAMZU_WORKING_DOCTRINE,
|
|
1210
|
+
NAMZU_DELEGATION_DOCTRINE,
|
|
1211
|
+
environmentPrompt,
|
|
1212
|
+
memoryPrompt,
|
|
1213
|
+
]
|
|
1214
|
+
.filter((s) => Boolean(s))
|
|
1215
|
+
.join('\n\n') || undefined;
|
|
1216
|
+
const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
1217
|
+
if (delegatedResumeHandlers.has(entry.runId)) {
|
|
1218
|
+
throw new Error(`Run ${entry.runId} already owns a delegated review channel.`);
|
|
1219
|
+
}
|
|
1220
|
+
delegatedResumeHandlers.set(entry.runId, resumeHandler);
|
|
1221
|
+
delegationScopes.set(entry.runId, { ...entry, topicId: scope.topicId });
|
|
1222
|
+
try {
|
|
1223
|
+
return await resumeRun({
|
|
1224
|
+
provider,
|
|
1225
|
+
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1226
|
+
tools: registry,
|
|
1227
|
+
pluginManager: pluginRuntime?.manager,
|
|
1228
|
+
skillRegistry: pluginRuntime?.skills,
|
|
1229
|
+
skills: pluginSkills,
|
|
1230
|
+
taskStore: selectTaskStore(entry.runId, { ...entry, topicId: scope.topicId }),
|
|
1231
|
+
// The same availability the original run registered under.
|
|
1232
|
+
// A resumed run re-registers the task tools; leaving them at
|
|
1233
|
+
// the kernel's `deferred` default would hand the model a plan
|
|
1234
|
+
// it started with active tools and can no longer update.
|
|
1235
|
+
runtimeToolOverrides: {
|
|
1236
|
+
task_create: 'active',
|
|
1237
|
+
task_update: 'active',
|
|
1238
|
+
task_list: 'active',
|
|
1239
|
+
},
|
|
1240
|
+
...(subagentRuntime
|
|
1241
|
+
? {
|
|
1242
|
+
taskScheduler: await subagentRuntime.gatewayForRun(entry.runId),
|
|
1243
|
+
}
|
|
1244
|
+
: {}),
|
|
1245
|
+
authorizationGate: gateFor(options.rules),
|
|
1246
|
+
compactionConfig: compactionConfigFor(options.compaction),
|
|
1247
|
+
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1248
|
+
pathBuilder,
|
|
1249
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1250
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1251
|
+
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
1252
|
+
: {}),
|
|
1253
|
+
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
1254
|
+
// whose `attach` detaches whoever held it before, so a caller
|
|
1255
|
+
// resuming several runs in one process would leave only the last
|
|
1256
|
+
// one covered — and would look covered. A turn owns its process
|
|
1257
|
+
// end to end; a drainer does not.
|
|
1258
|
+
runConfig: {
|
|
1259
|
+
model,
|
|
1260
|
+
...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1261
|
+
timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
1262
|
+
tokenBudget: options.limits?.tokenBudget ?? 1_000_000,
|
|
1263
|
+
maxIterations: options.limits?.maxIterations ?? 50,
|
|
1264
|
+
maxResponseTokens: 8192,
|
|
1265
|
+
permissionMode: 'auto',
|
|
1266
|
+
},
|
|
1267
|
+
agentId: 'namzu',
|
|
1268
|
+
agentName: 'namzu',
|
|
1269
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
1270
|
+
workingDirectory: cwd,
|
|
1271
|
+
...(directories.length > 0 ? { additionalDirectories: [...directories] } : {}),
|
|
1272
|
+
...(options.limits ? { limits: options.limits } : {}),
|
|
1273
|
+
// No `onPermission`: there is nobody at a drainer's terminal, so a
|
|
1274
|
+
// prompt would block the pass forever on a run nobody is watching.
|
|
1275
|
+
// The gate's deny rules still apply.
|
|
1276
|
+
// One presenter for the whole stream, built from the registry this
|
|
1277
|
+
// scope already holds. It was the absence of the registry HERE that
|
|
1278
|
+
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
1279
|
+
// over a `RunEvent` and could not ask a tool anything.
|
|
1280
|
+
resumeHandler,
|
|
1281
|
+
signal: ownedSignal,
|
|
1282
|
+
// Attribution comes from the ENTRY, not from this session: the run
|
|
1283
|
+
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
1284
|
+
// it would file another tenant's work under this one.
|
|
1285
|
+
tenantId: entry.tenantId,
|
|
1286
|
+
projectId: entry.projectId,
|
|
1287
|
+
sessionId: entry.sessionId,
|
|
1288
|
+
// …except the topic, which no checkpoint records — see
|
|
1289
|
+
// `RunStateScope`. This one is the drainer's, and honestly so:
|
|
1290
|
+
// supplied here rather than pretended to have been recovered.
|
|
1291
|
+
topicId: scope.topicId,
|
|
1292
|
+
scope: { ...entry, topicId: scope.topicId },
|
|
1293
|
+
checkpointStore,
|
|
1294
|
+
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1295
|
+
...(checkpointId !== undefined ? { checkpointId } : {}),
|
|
1296
|
+
...(listener ? { listener } : {}),
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
finally {
|
|
1300
|
+
if (delegatedResumeHandlers.get(entry.runId) === resumeHandler) {
|
|
1301
|
+
delegatedResumeHandlers.delete(entry.runId);
|
|
1302
|
+
delegationScopes.delete(entry.runId);
|
|
1303
|
+
await subagentRuntime?.releaseRun(entry.runId);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
/**
|
|
1308
|
+
* `resumeRun` drains the loop and returns a settled run; the events go to a
|
|
1309
|
+
* listener. A small queue turns that into the stream `send` gives, so a
|
|
1310
|
+
* headless caller renders a resumed run exactly as it rendered the turn.
|
|
1311
|
+
*/
|
|
1312
|
+
const resumePausedStream = ({ runId, checkpointId, signal, }) => {
|
|
1313
|
+
const queue = [];
|
|
1314
|
+
let wake;
|
|
1315
|
+
let settled = false;
|
|
1316
|
+
let failure;
|
|
1317
|
+
const presenter = createToolPresenter(registry);
|
|
1318
|
+
// The store the turn's run manager wrote to, built the same way it
|
|
1319
|
+
// built it (see the kernel's `RunPersistence`): the session directory's
|
|
1320
|
+
// `runs/`, attributed to this tenant and project.
|
|
1321
|
+
const store = new DiskCheckpointStore({
|
|
1322
|
+
baseDir: join(pathBuilder.sessionDir(scope.projectId, scope.sessionId), 'runs'),
|
|
1323
|
+
}, {
|
|
1324
|
+
tenantId: scope.tenantId,
|
|
1325
|
+
projectId: scope.projectId,
|
|
1326
|
+
sessionId: scope.sessionId,
|
|
1327
|
+
});
|
|
1328
|
+
const entry = {
|
|
1329
|
+
tenantId: scope.tenantId,
|
|
1330
|
+
projectId: scope.projectId,
|
|
1331
|
+
sessionId: scope.sessionId,
|
|
1332
|
+
runId: runId,
|
|
1333
|
+
};
|
|
1334
|
+
const outcome = kernelResume({
|
|
1335
|
+
entry,
|
|
1336
|
+
checkpointStore: store,
|
|
1337
|
+
...(signal ? { signal } : {}),
|
|
1338
|
+
checkpointId: checkpointId,
|
|
1339
|
+
listener: (event) => {
|
|
1340
|
+
queue.push(event);
|
|
1341
|
+
wake?.();
|
|
1342
|
+
},
|
|
1343
|
+
})
|
|
1344
|
+
.then((result) => {
|
|
1345
|
+
if (!result.resumed) {
|
|
1346
|
+
failure = new Error(result.reason === 'no-checkpoint'
|
|
1347
|
+
? `no checkpoint ${checkpointId} is recorded for run ${runId}`
|
|
1348
|
+
: `run ${runId} is parked on a decision only a person can answer`);
|
|
1349
|
+
}
|
|
1350
|
+
})
|
|
1351
|
+
.catch((err) => {
|
|
1352
|
+
failure = err instanceof Error ? err : new Error(String(err));
|
|
1353
|
+
})
|
|
1354
|
+
.finally(() => {
|
|
1355
|
+
settled = true;
|
|
1356
|
+
wake?.();
|
|
1357
|
+
});
|
|
1358
|
+
return (async function* () {
|
|
1359
|
+
for (;;) {
|
|
1360
|
+
while (queue.length > 0) {
|
|
1361
|
+
const next = queue.shift();
|
|
1362
|
+
if (!next)
|
|
1363
|
+
break;
|
|
1364
|
+
const mapped = toAgentEvent(next, presenter);
|
|
1365
|
+
if (mapped)
|
|
1366
|
+
yield mapped;
|
|
1367
|
+
}
|
|
1368
|
+
if (settled)
|
|
1369
|
+
break;
|
|
1370
|
+
await new Promise((resolve) => {
|
|
1371
|
+
wake = resolve;
|
|
1372
|
+
});
|
|
1373
|
+
wake = undefined;
|
|
1374
|
+
}
|
|
1375
|
+
await outcome;
|
|
1376
|
+
if (failure)
|
|
1377
|
+
yield { kind: 'error', message: failure.message };
|
|
1378
|
+
})();
|
|
1379
|
+
};
|
|
1035
1380
|
return {
|
|
1036
1381
|
hasProvider: true,
|
|
1037
1382
|
sandbox: {
|
|
@@ -1049,7 +1394,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1049
1394
|
await prepareProviderCredential(signal);
|
|
1050
1395
|
return compactNow({
|
|
1051
1396
|
messages,
|
|
1052
|
-
config:
|
|
1397
|
+
config: compactionConfigFor(options.compaction),
|
|
1053
1398
|
provider,
|
|
1054
1399
|
model,
|
|
1055
1400
|
signal,
|
|
@@ -1063,6 +1408,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1063
1408
|
.map((t) => t.name)
|
|
1064
1409
|
.filter((name) => !goalToolNames.has(name)),
|
|
1065
1410
|
agentIds: allowedAgentIds,
|
|
1411
|
+
currentTaskStore,
|
|
1412
|
+
resetTaskStore,
|
|
1413
|
+
jobs: () => jobRegistry?.list(jobOwner) ?? [],
|
|
1414
|
+
...(options.hooks ? { hooks: options.hooks } : {}),
|
|
1415
|
+
checkpoints,
|
|
1416
|
+
directories: sessionDirectories,
|
|
1417
|
+
onJobExit: (listener) => jobRegistry?.onExit((job) => {
|
|
1418
|
+
if (job.owner === jobOwner)
|
|
1419
|
+
listener(job);
|
|
1420
|
+
}) ?? (() => { }),
|
|
1066
1421
|
...(subagentRuntime ? { subagents: subagentRuntime.activity } : {}),
|
|
1067
1422
|
get instructionFiles() {
|
|
1068
1423
|
return projectInstructions.instructionFiles;
|
|
@@ -1096,6 +1451,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1096
1451
|
},
|
|
1097
1452
|
promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
|
|
1098
1453
|
send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
|
|
1454
|
+
const selectTaskStore = beginTaskStoreReadout();
|
|
1099
1455
|
const runId = opts?.runId ?? generateRunId();
|
|
1100
1456
|
const turnOpts = { ...opts, runId, signal };
|
|
1101
1457
|
const resumeHandler = makeResumeHandler(approval, opts?.onPermission, opts?.permissionMode ?? options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
@@ -1103,6 +1459,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1103
1459
|
throw new Error(`Run ${runId} already owns a delegated review channel.`);
|
|
1104
1460
|
}
|
|
1105
1461
|
delegatedResumeHandlers.set(runId, resumeHandler);
|
|
1462
|
+
if (opts?.waitForInbound)
|
|
1463
|
+
delegatedInputWaiters.set(runId, opts.waitForInbound);
|
|
1464
|
+
const turnScope = { ...scope };
|
|
1465
|
+
delegationScopes.set(runId, turnScope);
|
|
1106
1466
|
try {
|
|
1107
1467
|
// Renew a lapsed OAuth token before the turn runs (no-op for valid
|
|
1108
1468
|
// tokens and non-subscription credentials).
|
|
@@ -1121,13 +1481,16 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1121
1481
|
const pluginSkills = pluginRuntime
|
|
1122
1482
|
? await currentPluginSkills(pluginRuntime.skills)
|
|
1123
1483
|
: undefined;
|
|
1124
|
-
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
1484
|
+
const memoryPrompt = composeMemoryPrompt(readMemory(undefined, cwd));
|
|
1125
1485
|
currentOnQuestion = opts?.onQuestion;
|
|
1126
1486
|
const [environmentFacts, turnSnapshot] = await Promise.all([
|
|
1127
1487
|
readEnvironmentFacts(cwd),
|
|
1128
1488
|
readTurnSnapshot(cwd),
|
|
1129
1489
|
]);
|
|
1130
|
-
const environmentPrompt = composeEnvironmentPrompt(
|
|
1490
|
+
const environmentPrompt = composeEnvironmentPrompt({
|
|
1491
|
+
...environmentFacts,
|
|
1492
|
+
additionalDirectories: [...directories],
|
|
1493
|
+
});
|
|
1131
1494
|
// The repository as it stood when THIS turn began, through the
|
|
1132
1495
|
// SDK's `turn` placement — the ephemeral trailing message that is
|
|
1133
1496
|
// never cached and never enters history. FIRST iteration only:
|
|
@@ -1162,6 +1525,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1162
1525
|
]
|
|
1163
1526
|
.filter((s) => Boolean(s))
|
|
1164
1527
|
.join('\n\n') || undefined;
|
|
1528
|
+
await announceSessionStart();
|
|
1529
|
+
checkpoints.beginTurn(lastUserText(messages));
|
|
1165
1530
|
let capturedAuthority;
|
|
1166
1531
|
if (opts?.goalRound) {
|
|
1167
1532
|
if (!opts.runId)
|
|
@@ -1191,6 +1556,14 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1191
1556
|
try {
|
|
1192
1557
|
yield* runTurn({
|
|
1193
1558
|
provider,
|
|
1559
|
+
compactionConfig: compactionConfigFor(options.compaction),
|
|
1560
|
+
...(options.compaction?.consolidate ? { consolidateInto: memoryStore } : {}),
|
|
1561
|
+
...(jobRegistry
|
|
1562
|
+
? {
|
|
1563
|
+
backgroundJobs: jobRegistry,
|
|
1564
|
+
backgroundJobOwner: jobOwner,
|
|
1565
|
+
}
|
|
1566
|
+
: {}),
|
|
1194
1567
|
// Constructed HERE, per turn, and that is not an optimisation to
|
|
1195
1568
|
// undo. `refreshTokenIfNeeded` above replaces the head's client
|
|
1196
1569
|
// object when an OAuth token rotates, so a member list built once at
|
|
@@ -1204,21 +1577,24 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1204
1577
|
pluginManager: pluginRuntime?.manager,
|
|
1205
1578
|
skillRegistry: pluginRuntime?.skills,
|
|
1206
1579
|
skills: pluginSkills,
|
|
1207
|
-
scope,
|
|
1580
|
+
scope: turnScope,
|
|
1208
1581
|
pathBuilder,
|
|
1209
1582
|
workingDirectory: cwd,
|
|
1583
|
+
...(directories.length > 0 ? { additionalDirectories: [...directories] } : {}),
|
|
1584
|
+
...(options.limits ? { limits: options.limits } : {}),
|
|
1210
1585
|
sandboxWorkspace,
|
|
1211
1586
|
rules: options.rules,
|
|
1212
1587
|
reviewAnswer: options.reviewAnswer,
|
|
1213
1588
|
maxAnswerReviews: options.maxAnswerReviews,
|
|
1214
1589
|
promoteMemory,
|
|
1215
|
-
taskStore,
|
|
1590
|
+
taskStore: selectTaskStore(runId, turnScope),
|
|
1216
1591
|
systemPrompt,
|
|
1217
1592
|
messages,
|
|
1218
1593
|
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1219
1594
|
opts: turnOpts,
|
|
1220
1595
|
resumeHandler,
|
|
1221
|
-
taskGateway:
|
|
1596
|
+
taskGateway: await subagentRuntime?.gatewayForRun(runId),
|
|
1597
|
+
completionInbox: await subagentRuntime?.completionInboxForRun(runId),
|
|
1222
1598
|
promptContributions,
|
|
1223
1599
|
...(webCapability ? { web: webCapability } : {}),
|
|
1224
1600
|
// Active, not deferred: the doctrine tells the model to open a
|
|
@@ -1249,109 +1625,19 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
1249
1625
|
finally {
|
|
1250
1626
|
if (delegatedResumeHandlers.get(runId) === resumeHandler) {
|
|
1251
1627
|
delegatedResumeHandlers.delete(runId);
|
|
1628
|
+
delegatedInputWaiters.delete(runId);
|
|
1629
|
+
delegationScopes.delete(runId);
|
|
1630
|
+
await subagentRuntime?.releaseRun(runId);
|
|
1252
1631
|
}
|
|
1253
1632
|
}
|
|
1254
1633
|
})()),
|
|
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
|
-
}
|
|
1634
|
+
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => kernelResume({
|
|
1635
|
+
entry,
|
|
1636
|
+
checkpointStore,
|
|
1637
|
+
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1638
|
+
...(signal ? { signal } : {}),
|
|
1354
1639
|
}),
|
|
1640
|
+
resumePaused: (params) => resumePausedStream(params),
|
|
1355
1641
|
};
|
|
1356
1642
|
}
|
|
1357
1643
|
function planFallbacks(members, detected) {
|
|
@@ -1631,18 +1917,26 @@ export async function listProviderModels(id, det) {
|
|
|
1631
1917
|
const listing = await describeProviderModels(id, det);
|
|
1632
1918
|
return listing.kind === 'ok' ? [...listing.models] : [];
|
|
1633
1919
|
}
|
|
1634
|
-
/**
|
|
1920
|
+
/** The newest user turn's text, for labels. */
|
|
1921
|
+
function lastUserText(messages) {
|
|
1922
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1923
|
+
const m = messages[i];
|
|
1924
|
+
if (m?.role === 'user' && typeof m.content === 'string')
|
|
1925
|
+
return m.content;
|
|
1926
|
+
}
|
|
1927
|
+
return '';
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* A scope for a session no host supplied one for: four minted ids. Minted
|
|
1931
|
+
* rather than spelled, because a spelled id is a place a typo hides and
|
|
1932
|
+
* these types accept either spelling until they are nominal.
|
|
1933
|
+
*/
|
|
1635
1934
|
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
1935
|
return {
|
|
1642
|
-
sessionId:
|
|
1643
|
-
topicId:
|
|
1644
|
-
projectId:
|
|
1645
|
-
tenantId:
|
|
1936
|
+
sessionId: generateSessionId(),
|
|
1937
|
+
topicId: generateTopicId(),
|
|
1938
|
+
projectId: generateProjectId(),
|
|
1939
|
+
tenantId: generateTenantId(),
|
|
1646
1940
|
};
|
|
1647
1941
|
}
|
|
1648
1942
|
// Pre-execution safety gate: hard-deny catastrophic shell patterns
|
|
@@ -1681,7 +1975,7 @@ function gateFor(rules) {
|
|
|
1681
1975
|
// from `runConfig.model`, which is the value the user actually chose. Pinning
|
|
1682
1976
|
// a number here would fix one window across every model the CLI can talk to.
|
|
1683
1977
|
const COMPACTION_CONFIG = {
|
|
1684
|
-
strategy: '
|
|
1978
|
+
strategy: 'salience',
|
|
1685
1979
|
// On, and this is the CLI making a choice rather than taking a default.
|
|
1686
1980
|
// A session's transcript is the only record of what was compacted away;
|
|
1687
1981
|
// the size trade this costs is the operator's to see and turn off.
|
|
@@ -1709,7 +2003,17 @@ const COMPACTION_CONFIG = {
|
|
|
1709
2003
|
maxCharsPerRequirement: 300,
|
|
1710
2004
|
maxCharsPerTask: 400,
|
|
1711
2005
|
};
|
|
1712
|
-
|
|
2006
|
+
/** The shipped configuration with the strategy the project chose, if it chose one. */
|
|
2007
|
+
function compactionConfigFor(compaction) {
|
|
2008
|
+
return {
|
|
2009
|
+
...COMPACTION_CONFIG,
|
|
2010
|
+
strategy: compaction?.strategy ?? COMPACTION_CONFIG.strategy,
|
|
2011
|
+
...(compaction?.contextWindowTokens !== undefined
|
|
2012
|
+
? { contextWindowTokens: compaction.contextWindowTokens }
|
|
2013
|
+
: {}),
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
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, completionInbox, promptContributions, runtimeToolOverrides, web, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
|
|
1713
2017
|
const signal = opts?.signal;
|
|
1714
2018
|
// One presenter for the whole stream, built from the registry this scope
|
|
1715
2019
|
// already holds. Its absence HERE is what forced presentation to be name
|
|
@@ -1733,7 +2037,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1733
2037
|
// turn. An admitted send owns the exact run-scoped authority above.
|
|
1734
2038
|
...(!opts?.goalRound ? { deniedTools: SESSION_GOAL_TOOL_NAMES } : {}),
|
|
1735
2039
|
taskStore,
|
|
1736
|
-
...(taskGateway ? { taskGateway } : {}),
|
|
2040
|
+
...(taskGateway ? { taskScheduler: taskGateway } : {}),
|
|
1737
2041
|
// `gateFor`, not the bare default: the default's `rules` is a hardcoded
|
|
1738
2042
|
// empty array, so passing it here discarded the operator's rules on the
|
|
1739
2043
|
// path that runs every top-level turn. The sub-agent path called
|
|
@@ -1741,7 +2045,9 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1741
2045
|
...(sandboxProvider ? { sandboxProvider } : {}),
|
|
1742
2046
|
...(sandboxTeardownTimeoutMs !== undefined ? { sandboxTeardownTimeoutMs } : {}),
|
|
1743
2047
|
authorizationGate: gateFor(rules),
|
|
1744
|
-
compactionConfig
|
|
2048
|
+
compactionConfig,
|
|
2049
|
+
...(consolidateInto ? { consolidateInto } : {}),
|
|
2050
|
+
...(backgroundJobs ? { backgroundJobs, backgroundJobOwner } : {}),
|
|
1745
2051
|
// The CLI owns its process end to end, so it can safely hand the
|
|
1746
2052
|
// termination path to the kernel: a Ctrl-C mid-run now leaves a
|
|
1747
2053
|
// dump under the injected hierarchy's emergency partition instead of
|
|
@@ -1752,8 +2058,8 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1752
2058
|
...(sandboxProvider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1753
2059
|
...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
|
|
1754
2060
|
timeoutMs: CLI_INTERACTIVE_RUN_TIMEOUT_MS,
|
|
1755
|
-
tokenBudget: 1_000_000,
|
|
1756
|
-
maxIterations: 50,
|
|
2061
|
+
tokenBudget: limits?.tokenBudget ?? 1_000_000,
|
|
2062
|
+
maxIterations: limits?.maxIterations ?? 50,
|
|
1757
2063
|
maxResponseTokens: 8192,
|
|
1758
2064
|
permissionMode: 'auto',
|
|
1759
2065
|
},
|
|
@@ -1773,7 +2079,10 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1773
2079
|
projectInstructionContext,
|
|
1774
2080
|
messages: [...messages],
|
|
1775
2081
|
...(opts?.inboundMessages ? { inboundMessages: opts.inboundMessages } : {}),
|
|
2082
|
+
...(opts?.waitForInbound ? { waitForInbound: opts.waitForInbound } : {}),
|
|
2083
|
+
...(completionInbox ? { completionInbox } : {}),
|
|
1776
2084
|
workingDirectory,
|
|
2085
|
+
...(additionalDirectories?.length ? { additionalDirectories } : {}),
|
|
1777
2086
|
// The exemption reads `tools` at decision time, so it sees the task
|
|
1778
2087
|
// tools `query()` registers deferred below and any tool server that
|
|
1779
2088
|
// connected after this session was built.
|
|
@@ -1785,6 +2094,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1785
2094
|
...scope,
|
|
1786
2095
|
});
|
|
1787
2096
|
let settled = false;
|
|
2097
|
+
let abortReported = false;
|
|
1788
2098
|
try {
|
|
1789
2099
|
while (true) {
|
|
1790
2100
|
const next = await events.next();
|
|
@@ -1805,8 +2115,14 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1805
2115
|
// be a recording of the interface rather than of the session.
|
|
1806
2116
|
onRunEvent?.(event);
|
|
1807
2117
|
if (signal?.aborted) {
|
|
1808
|
-
|
|
1809
|
-
|
|
2118
|
+
if (!abortReported) {
|
|
2119
|
+
abortReported = true;
|
|
2120
|
+
yield { kind: 'error', message: 'aborted' };
|
|
2121
|
+
}
|
|
2122
|
+
// Let cancellation settle in the kernel. Calling return() here
|
|
2123
|
+
// discarded its Run and forced App to save only visible prose,
|
|
2124
|
+
// losing tool receipts and reasoning before the next user turn.
|
|
2125
|
+
continue;
|
|
1810
2126
|
}
|
|
1811
2127
|
const mapped = toAgentEvent(event, presenter);
|
|
1812
2128
|
if (!mapped)
|
|
@@ -1830,143 +2146,26 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1830
2146
|
}
|
|
1831
2147
|
}
|
|
1832
2148
|
/**
|
|
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.
|
|
2149
|
+
* The kernel's review policy with the TUI's prompt behind it.
|
|
1916
2150
|
*
|
|
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.
|
|
2151
|
+
* The five modes, the exemptions and the batch rule live in `@namzu/sdk`
|
|
2152
|
+
* (`createReviewHandler`); what this application adds is the person to ask
|
|
2153
|
+
* and the session's "approve all" box, which the screen also reads.
|
|
1934
2154
|
*/
|
|
1935
|
-
|
|
2155
|
+
export function makeResumeHandler(approval, onPermission, mode = onPermission ? 'prompt' : 'auto', exempt = () => false) {
|
|
2156
|
+
return createReviewHandler({
|
|
2157
|
+
mode,
|
|
2158
|
+
prompt: onPermission,
|
|
2159
|
+
exempt,
|
|
2160
|
+
remembered: approval,
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
1936
2163
|
/**
|
|
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.
|
|
2164
|
+
* Whether a call runs without asking. The kernel's rule: a trusted read-only
|
|
2165
|
+
* declaration or a named bookkeeping write, never a fetch, never a tool the
|
|
2166
|
+
* registry does not know.
|
|
1954
2167
|
*/
|
|
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
|
-
}
|
|
2168
|
+
export const isPromptExempt = isReviewExempt;
|
|
1970
2169
|
/** The exempt roster, sorted, for the surface that has to NAME it. */
|
|
1971
2170
|
export function promptExemptToolNames(registry) {
|
|
1972
2171
|
return registry
|
|
@@ -1975,13 +2174,8 @@ export function promptExemptToolNames(registry) {
|
|
|
1975
2174
|
.map((t) => t.name)
|
|
1976
2175
|
.sort();
|
|
1977
2176
|
}
|
|
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
|
-
}
|
|
2177
|
+
/** A batch needs explicit approval when any call mutates state. */
|
|
2178
|
+
export const batchNeedsPrompt = batchNeedsReview;
|
|
1985
2179
|
/**
|
|
1986
2180
|
* Translate one SDK `RunEvent` into the TUI's `AgentEvent` vocabulary, or
|
|
1987
2181
|
* `null` for events the chat surface doesn't render (iteration markers,
|
|
@@ -1996,6 +2190,14 @@ export function toAgentEvent(event, presenter) {
|
|
|
1996
2190
|
...(event.messageId ? { messageId: event.messageId } : {}),
|
|
1997
2191
|
...(event.runId ? { runId: event.runId } : {}),
|
|
1998
2192
|
};
|
|
2193
|
+
case 'reasoning_started':
|
|
2194
|
+
// A redacted block has no text to show; the empty delta still says
|
|
2195
|
+
// "thinking" so the region does not sit silent for its duration.
|
|
2196
|
+
return { kind: 'reasoning', text: '' };
|
|
2197
|
+
case 'reasoning_delta':
|
|
2198
|
+
return { kind: 'reasoning', text: event.text };
|
|
2199
|
+
case 'reasoning_completed':
|
|
2200
|
+
return { kind: 'reasoning', text: '', done: true };
|
|
1999
2201
|
case 'tool_executing':
|
|
2000
2202
|
return {
|
|
2001
2203
|
kind: 'tool-start',
|
|
@@ -2027,8 +2229,12 @@ export function toAgentEvent(event, presenter) {
|
|
|
2027
2229
|
success: !event.isError,
|
|
2028
2230
|
output: event.result,
|
|
2029
2231
|
});
|
|
2030
|
-
const summary = firstLine(event.result);
|
|
2031
2232
|
const detail = viewToLines(view);
|
|
2233
|
+
// Drop only an exact duplicate. A shortened summary cannot replace
|
|
2234
|
+
// the first line's evidence in expanded or raw output.
|
|
2235
|
+
const summary = view.kind === 'terminal' && detail && detail.length > 0
|
|
2236
|
+
? truncate(detail[0], 120)
|
|
2237
|
+
: firstLine(event.result);
|
|
2032
2238
|
const withoutRepeatedSummary = view.kind === 'terminal' && detail?.[0] === summary ? detail.slice(1) : detail;
|
|
2033
2239
|
return {
|
|
2034
2240
|
kind: 'tool-end',
|
|
@@ -2061,6 +2267,7 @@ export function toAgentEvent(event, presenter) {
|
|
|
2061
2267
|
return {
|
|
2062
2268
|
kind: 'usage',
|
|
2063
2269
|
totalTokens: event.usage.totalTokens,
|
|
2270
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2064
2271
|
cost: event.cost,
|
|
2065
2272
|
...(event.contextTokens !== undefined ? { contextTokens: event.contextTokens } : {}),
|
|
2066
2273
|
...(event.contextMeasuredBy !== undefined
|
|
@@ -2124,6 +2331,8 @@ export function toAgentEvent(event, presenter) {
|
|
|
2124
2331
|
// the SDK had explicitly stopped.
|
|
2125
2332
|
return {
|
|
2126
2333
|
kind: 'paused',
|
|
2334
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2335
|
+
runId: String(event.runId),
|
|
2127
2336
|
checkpointId: event.checkpointId,
|
|
2128
2337
|
reason: event.reason,
|
|
2129
2338
|
...(event.failure ? { failure: event.failure } : {}),
|
|
@@ -2138,6 +2347,7 @@ export function toAgentEvent(event, presenter) {
|
|
|
2138
2347
|
// answer was refused.
|
|
2139
2348
|
return {
|
|
2140
2349
|
kind: 'done',
|
|
2350
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2141
2351
|
...(event.stopReason ? { stopReason: event.stopReason } : {}),
|
|
2142
2352
|
};
|
|
2143
2353
|
case 'run_failed':
|
|
@@ -2147,13 +2357,29 @@ export function toAgentEvent(event, presenter) {
|
|
|
2147
2357
|
// still forcing every host to parse prose.
|
|
2148
2358
|
return {
|
|
2149
2359
|
kind: 'error',
|
|
2360
|
+
...(event.budget ? { budget: event.budget } : {}),
|
|
2150
2361
|
message: event.error,
|
|
2151
2362
|
...(event.failure ? { failure: event.failure } : {}),
|
|
2152
2363
|
...(event.providerError ? { providerError: event.providerError } : {}),
|
|
2153
2364
|
...(event.explanation ? { explanation: event.explanation } : {}),
|
|
2154
2365
|
};
|
|
2366
|
+
case 'background_job_exited':
|
|
2367
|
+
return {
|
|
2368
|
+
kind: 'job',
|
|
2369
|
+
jobId: event.jobId,
|
|
2370
|
+
command: event.command,
|
|
2371
|
+
status: event.status,
|
|
2372
|
+
...(event.exitCode !== undefined ? { exitCode: event.exitCode } : {}),
|
|
2373
|
+
...(event.signal ? { signal: event.signal } : {}),
|
|
2374
|
+
};
|
|
2155
2375
|
case 'compaction_completed':
|
|
2156
|
-
return {
|
|
2376
|
+
return {
|
|
2377
|
+
kind: 'context',
|
|
2378
|
+
text: describeCompaction(event),
|
|
2379
|
+
shed: true,
|
|
2380
|
+
summarised: true,
|
|
2381
|
+
reclaimedTokens: Math.max(0, event.tokensBefore - event.tokensAfter),
|
|
2382
|
+
};
|
|
2157
2383
|
case 'compaction_tool_results_cleared':
|
|
2158
2384
|
// `shed: true` on both branches: the tool-result bodies are gone
|
|
2159
2385
|
// either way. `reliefWasEnough: false` additionally means a
|
|
@@ -2161,8 +2387,11 @@ export function toAgentEvent(event, presenter) {
|
|
|
2161
2387
|
// so this one says what IT cost rather than claiming the total.
|
|
2162
2388
|
return {
|
|
2163
2389
|
kind: 'context',
|
|
2164
|
-
text: `cleared ${event.clearedCount}
|
|
2390
|
+
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
2391
|
shed: true,
|
|
2392
|
+
cleared: event.clearedCount,
|
|
2393
|
+
stubbed: event.stubbedCount ?? 0,
|
|
2394
|
+
reclaimedTokens: event.reclaimedTokens,
|
|
2166
2395
|
};
|
|
2167
2396
|
case 'compaction_failed':
|
|
2168
2397
|
return {
|
|
@@ -2267,7 +2496,7 @@ function describeFallback(event) {
|
|
|
2267
2496
|
* server's, a plugin's — could not get a diff no matter what it did.
|
|
2268
2497
|
*
|
|
2269
2498
|
* The tool now says which admitted shape it wants, and this decides what
|
|
2270
|
-
* that looks like in a terminal.
|
|
2499
|
+
* that looks like in a terminal. Collapsing and the `STDOUT:`/`STDERR:`
|
|
2271
2500
|
* cleanup stay here on purpose: how many rows fit and how a shell labels
|
|
2272
2501
|
* its streams are properties of this surface, not of the tool.
|
|
2273
2502
|
*/
|
|
@@ -2283,13 +2512,13 @@ export function viewToLines(view) {
|
|
|
2283
2512
|
// never produces this — it returns no view at all for an insert,
|
|
2284
2513
|
// rather than claim the file was empty.
|
|
2285
2514
|
if (view.before === '') {
|
|
2286
|
-
const lines =
|
|
2515
|
+
const lines = outputLines(view.after);
|
|
2287
2516
|
return lines.length > 0 ? lines : undefined;
|
|
2288
2517
|
}
|
|
2289
2518
|
const lines = [];
|
|
2290
|
-
for (const line of
|
|
2519
|
+
for (const line of outputLines(view.before))
|
|
2291
2520
|
lines.push(`- ${line}`);
|
|
2292
|
-
for (const line of
|
|
2521
|
+
for (const line of outputLines(view.after))
|
|
2293
2522
|
lines.push(`+ ${line}`);
|
|
2294
2523
|
return lines.length > 0 ? lines : undefined;
|
|
2295
2524
|
}
|
|
@@ -2298,7 +2527,7 @@ export function viewToLines(view) {
|
|
|
2298
2527
|
return undefined;
|
|
2299
2528
|
const lines = resultToLines(view.output);
|
|
2300
2529
|
// A single short line is already the summary — no need to repeat it.
|
|
2301
|
-
return lines.length
|
|
2530
|
+
return lines.length === 1 && lines[0] === truncate(lines[0] ?? '', 120) ? undefined : lines;
|
|
2302
2531
|
}
|
|
2303
2532
|
}
|
|
2304
2533
|
}
|
|
@@ -2317,10 +2546,10 @@ function truncate(value, max) {
|
|
|
2317
2546
|
const oneLine = value.replace(/\s+/g, ' ');
|
|
2318
2547
|
return oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine;
|
|
2319
2548
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
return
|
|
2549
|
+
function outputLines(value) {
|
|
2550
|
+
// The renderer bounds the preview. Retain admitted output so expanding
|
|
2551
|
+
// or selecting raw text never loses a diagnostic after an arbitrary line.
|
|
2552
|
+
return value.replace(/\s+$/, '').split('\n');
|
|
2324
2553
|
}
|
|
2325
2554
|
/** Parse a string as a JSON object, or null. Connector tools return JSON. */
|
|
2326
2555
|
function parseJsonObject(s) {
|
|
@@ -2362,11 +2591,11 @@ function payloadString(result) {
|
|
|
2362
2591
|
function resultToLines(result) {
|
|
2363
2592
|
const payload = payloadString(result);
|
|
2364
2593
|
if (payload !== null)
|
|
2365
|
-
return
|
|
2594
|
+
return outputLines(payload);
|
|
2366
2595
|
const obj = parseJsonObject(result);
|
|
2367
2596
|
if (obj)
|
|
2368
|
-
return
|
|
2369
|
-
return
|
|
2597
|
+
return outputLines(JSON.stringify(obj, null, 2));
|
|
2598
|
+
return outputLines(cleanToolText(result.trim()));
|
|
2370
2599
|
}
|
|
2371
2600
|
/** Concise one-line summary of a tool result for the `⎿` line. */
|
|
2372
2601
|
function firstLine(result) {
|
|
@@ -2502,6 +2731,9 @@ function emptySession(errorHint, errorKind = 'environment') {
|
|
|
2502
2731
|
resumeDurable: async () => {
|
|
2503
2732
|
throw new Error(errorHint);
|
|
2504
2733
|
},
|
|
2734
|
+
resumePaused: async function* () {
|
|
2735
|
+
yield { kind: 'error', message: 'no provider: nothing to resume' };
|
|
2736
|
+
},
|
|
2505
2737
|
close: async () => {
|
|
2506
2738
|
// Nothing was ever connected on this path.
|
|
2507
2739
|
},
|