@namzu/cli 15.1.2 → 16.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -7
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -8
- package/dist/cli.js.map +1 -1
- package/dist/commands/state.d.ts +9 -0
- package/dist/commands/state.d.ts.map +1 -0
- package/dist/commands/state.js +110 -0
- package/dist/commands/state.js.map +1 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +8 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/schema.d.ts +8 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/context/capabilities.d.ts +7 -4
- package/dist/context/capabilities.d.ts.map +1 -1
- package/dist/context/capabilities.js +7 -4
- package/dist/context/capabilities.js.map +1 -1
- package/dist/context/sandbox.d.ts +5 -0
- package/dist/context/sandbox.d.ts.map +1 -1
- package/dist/context/sandbox.js +11 -4
- package/dist/context/sandbox.js.map +1 -1
- package/dist/integrations/sessions/store.d.ts.map +1 -1
- package/dist/integrations/sessions/store.js +25 -2
- package/dist/integrations/sessions/store.js.map +1 -1
- package/dist/integrations/state/private-directory.d.ts +15 -0
- package/dist/integrations/state/private-directory.d.ts.map +1 -0
- package/dist/integrations/state/private-directory.js +41 -0
- package/dist/integrations/state/private-directory.js.map +1 -0
- package/dist/integrations/state/report.d.ts +125 -0
- package/dist/integrations/state/report.d.ts.map +1 -0
- package/dist/integrations/state/report.js +784 -0
- package/dist/integrations/state/report.js.map +1 -0
- package/dist/integrations/subagents/activity.d.ts +88 -0
- package/dist/integrations/subagents/activity.d.ts.map +1 -0
- package/dist/integrations/subagents/activity.js +389 -0
- package/dist/integrations/subagents/activity.js.map +1 -0
- package/dist/integrations/subagents/runtime.d.ts +11 -1
- package/dist/integrations/subagents/runtime.d.ts.map +1 -1
- package/dist/integrations/subagents/runtime.js +66 -3
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/tui/AgentExplorer.d.ts +57 -0
- package/dist/tui/AgentExplorer.d.ts.map +1 -0
- package/dist/tui/AgentExplorer.js +285 -0
- package/dist/tui/AgentExplorer.js.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +479 -150
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/Composer.d.ts +7 -1
- package/dist/tui/Composer.d.ts.map +1 -1
- package/dist/tui/Composer.js +26 -5
- package/dist/tui/Composer.js.map +1 -1
- package/dist/tui/LiveActivity.d.ts +9 -3
- package/dist/tui/LiveActivity.d.ts.map +1 -1
- package/dist/tui/LiveActivity.js +27 -10
- package/dist/tui/LiveActivity.js.map +1 -1
- package/dist/tui/PermissionOverlay.d.ts +8 -9
- package/dist/tui/PermissionOverlay.d.ts.map +1 -1
- package/dist/tui/PermissionOverlay.js +6 -10
- package/dist/tui/PermissionOverlay.js.map +1 -1
- package/dist/tui/StatusBar.d.ts +1 -47
- package/dist/tui/StatusBar.d.ts.map +1 -1
- package/dist/tui/StatusBar.js +9 -79
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/agent.d.ts +3 -0
- package/dist/tui/agent.d.ts.map +1 -1
- package/dist/tui/agent.js +217 -178
- package/dist/tui/agent.js.map +1 -1
- package/dist/tui/permission-review.d.ts +18 -2
- package/dist/tui/permission-review.d.ts.map +1 -1
- package/dist/tui/permission-review.js +88 -2
- package/dist/tui/permission-review.js.map +1 -1
- package/dist/tui/slashCommands.d.ts +4 -0
- package/dist/tui/slashCommands.d.ts.map +1 -1
- package/dist/tui/slashCommands.js +14 -0
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/user-commands/store.d.ts.map +1 -1
- package/dist/user-commands/store.js +26 -5
- package/dist/user-commands/store.js.map +1 -1
- package/package.json +2 -2
package/dist/tui/agent.js
CHANGED
|
@@ -20,9 +20,10 @@
|
|
|
20
20
|
* `emptySession()` whose `send()` yields a single error event so the UI
|
|
21
21
|
* renders an actionable hint rather than crashing.
|
|
22
22
|
*/
|
|
23
|
-
import { BOOT_EVENT_NAMES, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, asProjectId, asRunId, asSessionId, asTenantId, asTopicId, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createToolPresenter,
|
|
23
|
+
import { BOOT_EVENT_NAMES, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, asProjectId, asRunId, asSessionId, asTenantId, asTopicId, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createToolPresenter, generateRunId, getBuiltinTools, isTrustedReadOnly, query, resumeRun, withProviderFallback, } from '@namzu/sdk';
|
|
24
24
|
import { SubprocessComputerUseHost } from '@namzu/computer-use';
|
|
25
|
-
import {
|
|
25
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
26
|
+
import { join, parse, resolve } from 'node:path';
|
|
26
27
|
import { probeCapabilities } from '../context/capabilities.js';
|
|
27
28
|
import { composeEnvironmentPrompt, readEnvironmentFacts } from '../context/environment.js';
|
|
28
29
|
import { ProjectInstructionTracker } from '../context/project-tracker.js';
|
|
@@ -30,6 +31,7 @@ import { resolveSandbox, sandboxResolvedSeverity, } from '../context/sandbox.js'
|
|
|
30
31
|
import { connectMcpServers, } from '../integrations/mcp/servers.js';
|
|
31
32
|
import { createCliPluginRuntime } from '../integrations/plugins/runtime.js';
|
|
32
33
|
import { CredentialRefreshRejectedError, CredentialWithdrawnError, PROVIDER_REGISTRY, chainCapabilityDisagreements, chainPositionName, describeAcceptedMismatch, describeCapabilityRefusal, discoverProviders, ensureFreshAnthropicToken, ensureFreshStoredCodexCredential, ensureRegistered, findDetected, isAnthropicOAuthToken, isRegistered, missingCredentialMessage, primaryProvider, readCodexCredentialFile, readPreferences, readSubscriptionCredential, resolveChainCapabilities, sameOAuthCredential, unresolvedMembers, unsupportedProviderMessage, } from '../integrations/providers/index.js';
|
|
34
|
+
import { ensurePrivateStateDirectory } from '../integrations/state/private-directory.js';
|
|
33
35
|
import { createSubagentRuntime } from '../integrations/subagents/runtime.js';
|
|
34
36
|
import { cliLogger } from '../logging.js';
|
|
35
37
|
import { composeMemoryPrompt, readMemory } from '../memory/store.js';
|
|
@@ -326,7 +328,9 @@ function buildToolRegistry(cwd) {
|
|
|
326
328
|
// `save_memory` look like it touched only namzu's own state and was part of
|
|
327
329
|
// why it sat on the no-prompt list; it writes into the user's project.
|
|
328
330
|
// Separate from the user-curated MEMORY.md that is injected into the prompt.
|
|
329
|
-
const
|
|
331
|
+
const stateRoot = join(cwd, '.namzu');
|
|
332
|
+
ensurePrivateStateDirectory(stateRoot, 'memory');
|
|
333
|
+
const memoryStore = new DiskMemoryStore({ baseDir: stateRoot });
|
|
330
334
|
// Search through the store's async boundary. Its concrete index is lazy:
|
|
331
335
|
// handing `getIndex()` to the synchronous overload before the first store
|
|
332
336
|
// read makes a new process report every persisted memory as absent.
|
|
@@ -350,7 +354,35 @@ async function currentPluginSkills(registry) {
|
|
|
350
354
|
}
|
|
351
355
|
export async function createAgentSession(prefs, detected, options = {}) {
|
|
352
356
|
const scope = options.scope ?? mintScope();
|
|
353
|
-
const
|
|
357
|
+
const requestedCwd = resolve(options.cwd ?? process.cwd());
|
|
358
|
+
let cwd;
|
|
359
|
+
try {
|
|
360
|
+
const entry = await stat(requestedCwd);
|
|
361
|
+
if (!entry.isDirectory()) {
|
|
362
|
+
return emptySession(`Working directory is not a directory: ${requestedCwd}`, 'invocation');
|
|
363
|
+
}
|
|
364
|
+
cwd = await realpath(requestedCwd);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
return emptySession(`Working directory is unavailable: ${requestedCwd} — ${describeError(error)}`, 'invocation');
|
|
368
|
+
}
|
|
369
|
+
const sandboxWorkspace = options.sandbox?.workspace ?? 'working-directory';
|
|
370
|
+
if (options.sandbox?.enabled !== false &&
|
|
371
|
+
sandboxWorkspace === 'working-directory' &&
|
|
372
|
+
cwd === parse(cwd).root) {
|
|
373
|
+
return emptySession(`Working-directory sandboxing refuses filesystem root ${cwd}; choose a project directory so confinement has a boundary, or explicitly select an ephemeral workspace.`, 'invocation');
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
// Refuse an aliased or otherwise unsafe generated-state root before any
|
|
377
|
+
// provider, sandbox or plugin runtime is constructed. Project-authored
|
|
378
|
+
// `.namzu` content may coexist here, but generated memory must never be
|
|
379
|
+
// redirected outside the trusted working directory through an ancestor
|
|
380
|
+
// symlink.
|
|
381
|
+
ensurePrivateStateDirectory(join(cwd, '.namzu'), 'memory');
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
return emptySession(`Project state is unavailable: ${describeError(error)}`, 'environment');
|
|
385
|
+
}
|
|
354
386
|
// The head serves; the tail is fallen over to, in order, when it cannot.
|
|
355
387
|
const primary = primaryProvider(prefs);
|
|
356
388
|
const entry = PROVIDER_REGISTRY[primary.id];
|
|
@@ -662,6 +694,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
662
694
|
// `buildToolRegistry` directly below, so they never receive these tools.
|
|
663
695
|
// Per-send denial further keeps the schemas out of ordinary human turns.
|
|
664
696
|
const goalAuthorities = new Map();
|
|
697
|
+
// A child is created after the parent query has started, from inside its
|
|
698
|
+
// Agent tool. Keying the review channel by the executing run keeps two
|
|
699
|
+
// concurrent sends from borrowing each other's prompt or approval latch.
|
|
700
|
+
const delegatedResumeHandlers = new Map();
|
|
665
701
|
const goalToolNames = new Set(SESSION_GOAL_TOOL_NAMES);
|
|
666
702
|
if (options.sessionGoals) {
|
|
667
703
|
registry.register(buildSessionGoalTools(options.sessionGoals, (runId) => goalAuthorities.get(runId)));
|
|
@@ -714,11 +750,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
714
750
|
// Best-effort — if the runtime can't stand up, the chat still works.
|
|
715
751
|
let subagentGateway;
|
|
716
752
|
let subagentRuntime;
|
|
717
|
-
// Buffer of the in-flight sub-agent's tool steps. The gateway streams the
|
|
718
|
-
// child's events here while the parent's `Agent` tool call blocks; runTurn
|
|
719
|
-
// drains it onto the `Agent` result as a `├─/└─` tree. Scoped per `Agent`
|
|
720
|
-
// call (cleared when one starts).
|
|
721
|
-
const childSteps = [];
|
|
722
753
|
// Stays empty when the runtime below throws, which is the honest answer: the
|
|
723
754
|
// catch is non-fatal and the session then genuinely has no delegate to
|
|
724
755
|
// dispatch to. A roster reported from the request rather than the result
|
|
@@ -728,6 +759,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
728
759
|
const sub = await createSubagentRuntime({
|
|
729
760
|
cwd,
|
|
730
761
|
model,
|
|
762
|
+
sandboxWorkspace,
|
|
763
|
+
resolveResumeHandler: (runId) => delegatedResumeHandlers.get(runId),
|
|
731
764
|
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
732
765
|
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
733
766
|
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
@@ -769,11 +802,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
769
802
|
return buildToolRegistry(cwd).registry;
|
|
770
803
|
},
|
|
771
804
|
authorizationGate: gateFor(options.rules),
|
|
772
|
-
onEvent: (e) => {
|
|
773
|
-
if (e.type === 'tool_executing') {
|
|
774
|
-
childSteps.push(`${e.toolName}(${genericLabel(e.input)})`);
|
|
775
|
-
}
|
|
776
|
-
},
|
|
777
805
|
});
|
|
778
806
|
subagentRuntime = sub;
|
|
779
807
|
registry.register([sub.agentTool]);
|
|
@@ -803,8 +831,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
803
831
|
// It is also why `toolNames` below reads the registry rather than a list
|
|
804
832
|
// captured on this line. The count at connect time is unchanged; what
|
|
805
833
|
// changes is that asking again later gets a later answer.
|
|
834
|
+
const stateRoot = join(cwd, '.namzu');
|
|
835
|
+
ensurePrivateStateDirectory(stateRoot, 'tenants');
|
|
806
836
|
const taskStore = new DiskTaskStore({
|
|
807
|
-
baseDir:
|
|
837
|
+
baseDir: stateRoot,
|
|
808
838
|
defaultRunId: asRunId('run_namzu-cli'),
|
|
809
839
|
tenantId: scope.tenantId,
|
|
810
840
|
});
|
|
@@ -905,6 +935,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
905
935
|
...(sandbox.environment ? { environment: sandbox.environment } : {}),
|
|
906
936
|
enforced: sandbox.enforced,
|
|
907
937
|
required: sandbox.required,
|
|
938
|
+
workspace: sandbox.workspace,
|
|
908
939
|
},
|
|
909
940
|
providerSummary: entry.label,
|
|
910
941
|
modelSummary: model,
|
|
@@ -928,6 +959,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
928
959
|
.map((t) => t.name)
|
|
929
960
|
.filter((name) => !goalToolNames.has(name)),
|
|
930
961
|
agentIds: allowedAgentIds,
|
|
962
|
+
...(subagentRuntime ? { subagents: subagentRuntime.activity } : {}),
|
|
931
963
|
get instructionFiles() {
|
|
932
964
|
return projectInstructions.instructionFiles;
|
|
933
965
|
},
|
|
@@ -960,172 +992,198 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
960
992
|
},
|
|
961
993
|
promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
|
|
962
994
|
send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
995
|
+
const runId = opts?.runId ?? generateRunId();
|
|
996
|
+
const turnOpts = { ...opts, runId, signal };
|
|
997
|
+
const resumeHandler = makeResumeHandler(approval, opts?.onPermission, opts?.permissionMode ?? options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
998
|
+
if (delegatedResumeHandlers.has(runId)) {
|
|
999
|
+
throw new Error(`Run ${runId} already owns a delegated review channel.`);
|
|
1000
|
+
}
|
|
1001
|
+
delegatedResumeHandlers.set(runId, resumeHandler);
|
|
1002
|
+
try {
|
|
1003
|
+
// Renew a lapsed OAuth token before the turn runs (no-op for valid
|
|
1004
|
+
// tokens and non-subscription credentials).
|
|
1005
|
+
await prepareProviderCredential(signal);
|
|
1006
|
+
// namzu identity first (so it establishes who the agent is even when
|
|
1007
|
+
// the credential layer prepends whatever prefix its token requires),
|
|
1008
|
+
// then memory and per-turn extra context. Project instructions are a
|
|
1009
|
+
// separate retained user-context snapshot prepared by the controller;
|
|
1010
|
+
// that is what lets nested scopes be replaced and persisted safely.
|
|
1011
|
+
//
|
|
1012
|
+
// The environment block is read fresh every turn because both facts in it
|
|
1013
|
+
// can change WHILE the session
|
|
1014
|
+
// runs — midnight passes, and the agent checks out a branch itself.
|
|
1015
|
+
// Its text only changes when a fact changes, so it costs a prompt-cache
|
|
1016
|
+
// miss exactly when a hit would have been a stale claim.
|
|
1017
|
+
const pluginSkills = pluginRuntime
|
|
1018
|
+
? await currentPluginSkills(pluginRuntime.skills)
|
|
1019
|
+
: undefined;
|
|
1020
|
+
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
1021
|
+
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
1022
|
+
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt, opts?.extraSystem]
|
|
1023
|
+
.filter((s) => Boolean(s))
|
|
1024
|
+
.join('\n\n') || undefined;
|
|
1025
|
+
let capturedAuthority;
|
|
1026
|
+
if (opts?.goalRound) {
|
|
1027
|
+
if (!opts.runId)
|
|
1028
|
+
throw new Error('A goal round requires a caller-reserved runId.');
|
|
1029
|
+
if (!options.sessionGoals)
|
|
1030
|
+
throw new Error('This session has no durable goal store.');
|
|
1031
|
+
if (opts.goalRound.sessionId !== scope.sessionId ||
|
|
1032
|
+
opts.goalRound.tenantId !== scope.tenantId) {
|
|
1033
|
+
throw new Error('Goal-round authority does not belong to this agent session scope.');
|
|
1034
|
+
}
|
|
1035
|
+
const current = await options.sessionGoals.getGoal(scope.sessionId, scope.tenantId);
|
|
1036
|
+
if (!current ||
|
|
1037
|
+
current.phase !== 'active' ||
|
|
1038
|
+
current.id !== opts.goalRound.id ||
|
|
1039
|
+
current.revision !== opts.goalRound.revision ||
|
|
1040
|
+
current.objective !== opts.goalRound.objective ||
|
|
1041
|
+
current.roundsAdmitted !== opts.goalRound.round ||
|
|
1042
|
+
current.maxGoalRounds !== opts.goalRound.maxGoalRounds) {
|
|
1043
|
+
throw new Error('Goal-round authority is stale or does not match the durable goal.');
|
|
1044
|
+
}
|
|
1045
|
+
if (goalAuthorities.has(opts.runId)) {
|
|
1046
|
+
throw new Error(`Run ${opts.runId} already owns goal-round authority.`);
|
|
1047
|
+
}
|
|
1048
|
+
capturedAuthority = Object.freeze({ ...opts.goalRound });
|
|
1049
|
+
goalAuthorities.set(opts.runId, capturedAuthority);
|
|
1050
|
+
}
|
|
1051
|
+
try {
|
|
1052
|
+
yield* runTurn({
|
|
1053
|
+
provider,
|
|
1054
|
+
// Constructed HERE, per turn, and that is not an optimisation to
|
|
1055
|
+
// undo. `refreshTokenIfNeeded` above replaces the head's client
|
|
1056
|
+
// object when an OAuth token rotates, so a member list built once at
|
|
1057
|
+
// session creation would hand the kernel a client holding a token
|
|
1058
|
+
// that expired hours ago — and a chain whose own members are stale
|
|
1059
|
+
// is a fallback that fails for the reason the fallback exists to
|
|
1060
|
+
// survive. Building a driver is a client object, not a request.
|
|
1061
|
+
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1062
|
+
model,
|
|
1063
|
+
tools: registry,
|
|
1064
|
+
pluginManager: pluginRuntime?.manager,
|
|
1065
|
+
skillRegistry: pluginRuntime?.skills,
|
|
1066
|
+
skills: pluginSkills,
|
|
1067
|
+
scope,
|
|
1068
|
+
workingDirectory: cwd,
|
|
1069
|
+
sandboxWorkspace,
|
|
1070
|
+
rules: options.rules,
|
|
1071
|
+
reviewAnswer: options.reviewAnswer,
|
|
1072
|
+
maxAnswerReviews: options.maxAnswerReviews,
|
|
1073
|
+
promoteMemory,
|
|
1074
|
+
taskStore,
|
|
1075
|
+
systemPrompt,
|
|
1076
|
+
messages,
|
|
1077
|
+
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1078
|
+
opts: turnOpts,
|
|
1079
|
+
resumeHandler,
|
|
1080
|
+
taskGateway: subagentGateway,
|
|
1081
|
+
onRunEvent: options.onRunEvent,
|
|
1082
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1083
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1084
|
+
? {
|
|
1085
|
+
sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs,
|
|
1086
|
+
}
|
|
1087
|
+
: {}),
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
finally {
|
|
1091
|
+
if (opts?.runId &&
|
|
1092
|
+
capturedAuthority &&
|
|
1093
|
+
goalAuthorities.get(opts.runId) === capturedAuthority) {
|
|
1094
|
+
goalAuthorities.delete(opts.runId);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
finally {
|
|
1099
|
+
if (delegatedResumeHandlers.get(runId) === resumeHandler) {
|
|
1100
|
+
delegatedResumeHandlers.delete(runId);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
})()),
|
|
1104
|
+
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => operations.promise(signal, async (ownedSignal) => {
|
|
1105
|
+
// The same prelude a turn runs, and for the same reasons: a lapsed
|
|
1106
|
+
// OAuth token has to be renewed before the provider is used, and the
|
|
1107
|
+
// fallback chain has to be built AFTER that so its members do not
|
|
1108
|
+
// hold a client the refresh just replaced.
|
|
1109
|
+
await prepareProviderCredential(ownedSignal);
|
|
977
1110
|
const pluginSkills = pluginRuntime
|
|
978
1111
|
? await currentPluginSkills(pluginRuntime.skills)
|
|
979
1112
|
: undefined;
|
|
980
1113
|
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
981
1114
|
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
982
|
-
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt
|
|
1115
|
+
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
|
|
983
1116
|
.filter((s) => Boolean(s))
|
|
984
1117
|
.join('\n\n') || undefined;
|
|
985
|
-
|
|
986
|
-
if (
|
|
987
|
-
|
|
988
|
-
throw new Error('A goal round requires a caller-reserved runId.');
|
|
989
|
-
if (!options.sessionGoals)
|
|
990
|
-
throw new Error('This session has no durable goal store.');
|
|
991
|
-
if (opts.goalRound.sessionId !== scope.sessionId ||
|
|
992
|
-
opts.goalRound.tenantId !== scope.tenantId) {
|
|
993
|
-
throw new Error('Goal-round authority does not belong to this agent session scope.');
|
|
994
|
-
}
|
|
995
|
-
const current = await options.sessionGoals.getGoal(scope.sessionId, scope.tenantId);
|
|
996
|
-
if (!current ||
|
|
997
|
-
current.phase !== 'active' ||
|
|
998
|
-
current.id !== opts.goalRound.id ||
|
|
999
|
-
current.revision !== opts.goalRound.revision ||
|
|
1000
|
-
current.objective !== opts.goalRound.objective ||
|
|
1001
|
-
current.roundsAdmitted !== opts.goalRound.round ||
|
|
1002
|
-
current.maxGoalRounds !== opts.goalRound.maxGoalRounds) {
|
|
1003
|
-
throw new Error('Goal-round authority is stale or does not match the durable goal.');
|
|
1004
|
-
}
|
|
1005
|
-
if (goalAuthorities.has(opts.runId)) {
|
|
1006
|
-
throw new Error(`Run ${opts.runId} already owns goal-round authority.`);
|
|
1007
|
-
}
|
|
1008
|
-
capturedAuthority = Object.freeze({ ...opts.goalRound });
|
|
1009
|
-
goalAuthorities.set(opts.runId, capturedAuthority);
|
|
1118
|
+
const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
1119
|
+
if (delegatedResumeHandlers.has(entry.runId)) {
|
|
1120
|
+
throw new Error(`Run ${entry.runId} already owns a delegated review channel.`);
|
|
1010
1121
|
}
|
|
1122
|
+
delegatedResumeHandlers.set(entry.runId, resumeHandler);
|
|
1011
1123
|
try {
|
|
1012
|
-
|
|
1124
|
+
return await resumeRun({
|
|
1013
1125
|
provider,
|
|
1014
|
-
// Constructed HERE, per turn, and that is not an optimisation to
|
|
1015
|
-
// undo. `refreshTokenIfNeeded` above replaces the head's client
|
|
1016
|
-
// object when an OAuth token rotates, so a member list built once at
|
|
1017
|
-
// session creation would hand the kernel a client holding a token
|
|
1018
|
-
// that expired hours ago — and a chain whose own members are stale
|
|
1019
|
-
// is a fallback that fails for the reason the fallback exists to
|
|
1020
|
-
// survive. Building a driver is a client object, not a request.
|
|
1021
1126
|
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1022
|
-
model,
|
|
1023
1127
|
tools: registry,
|
|
1024
1128
|
pluginManager: pluginRuntime?.manager,
|
|
1025
1129
|
skillRegistry: pluginRuntime?.skills,
|
|
1026
1130
|
skills: pluginSkills,
|
|
1027
|
-
scope,
|
|
1028
|
-
workingDirectory: cwd,
|
|
1029
|
-
rules: options.rules,
|
|
1030
|
-
permissionMode: opts?.permissionMode ?? options.permissionMode,
|
|
1031
|
-
reviewAnswer: options.reviewAnswer,
|
|
1032
|
-
maxAnswerReviews: options.maxAnswerReviews,
|
|
1033
|
-
promoteMemory,
|
|
1034
|
-
approval,
|
|
1035
1131
|
taskStore,
|
|
1036
|
-
|
|
1037
|
-
|
|
1132
|
+
...(subagentGateway ? { taskGateway: subagentGateway } : {}),
|
|
1133
|
+
authorizationGate: gateFor(options.rules),
|
|
1134
|
+
compactionConfig: COMPACTION_CONFIG,
|
|
1038
1135
|
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1039
|
-
opts: { ...opts, signal },
|
|
1040
|
-
taskGateway: subagentGateway,
|
|
1041
|
-
onRunEvent: options.onRunEvent,
|
|
1042
|
-
childSteps,
|
|
1043
1136
|
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1044
1137
|
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1045
|
-
? {
|
|
1046
|
-
sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs,
|
|
1047
|
-
}
|
|
1138
|
+
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
1048
1139
|
: {}),
|
|
1140
|
+
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
1141
|
+
// whose `attach` detaches whoever held it before, so a caller
|
|
1142
|
+
// resuming several runs in one process would leave only the last
|
|
1143
|
+
// one covered — and would look covered. A turn owns its process
|
|
1144
|
+
// end to end; a drainer does not.
|
|
1145
|
+
runConfig: {
|
|
1146
|
+
model,
|
|
1147
|
+
...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1148
|
+
timeoutMs: 600_000,
|
|
1149
|
+
tokenBudget: 1_000_000,
|
|
1150
|
+
maxIterations: 50,
|
|
1151
|
+
maxResponseTokens: 8192,
|
|
1152
|
+
permissionMode: 'auto',
|
|
1153
|
+
},
|
|
1154
|
+
agentId: 'namzu',
|
|
1155
|
+
agentName: 'namzu',
|
|
1156
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
1157
|
+
workingDirectory: cwd,
|
|
1158
|
+
// No `onPermission`: there is nobody at a drainer's terminal, so a
|
|
1159
|
+
// prompt would block the pass forever on a run nobody is watching.
|
|
1160
|
+
// The gate's deny rules still apply.
|
|
1161
|
+
// One presenter for the whole stream, built from the registry this
|
|
1162
|
+
// scope already holds. It was the absence of the registry HERE that
|
|
1163
|
+
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
1164
|
+
// over a `RunEvent` and could not ask a tool anything.
|
|
1165
|
+
resumeHandler,
|
|
1166
|
+
signal: ownedSignal,
|
|
1167
|
+
// Attribution comes from the ENTRY, not from this session: the run
|
|
1168
|
+
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
1169
|
+
// it would file another tenant's work under this one.
|
|
1170
|
+
tenantId: entry.tenantId,
|
|
1171
|
+
projectId: entry.projectId,
|
|
1172
|
+
sessionId: entry.sessionId,
|
|
1173
|
+
// …except the topic, which no checkpoint records — see
|
|
1174
|
+
// `RunStateScope`. This one is the drainer's, and honestly so:
|
|
1175
|
+
// supplied here rather than pretended to have been recovered.
|
|
1176
|
+
topicId: scope.topicId,
|
|
1177
|
+
scope: { ...entry, topicId: scope.topicId },
|
|
1178
|
+
checkpointStore,
|
|
1179
|
+
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1049
1180
|
});
|
|
1050
1181
|
}
|
|
1051
1182
|
finally {
|
|
1052
|
-
if (
|
|
1053
|
-
|
|
1054
|
-
goalAuthorities.get(opts.runId) === capturedAuthority) {
|
|
1055
|
-
goalAuthorities.delete(opts.runId);
|
|
1183
|
+
if (delegatedResumeHandlers.get(entry.runId) === resumeHandler) {
|
|
1184
|
+
delegatedResumeHandlers.delete(entry.runId);
|
|
1056
1185
|
}
|
|
1057
1186
|
}
|
|
1058
|
-
})()),
|
|
1059
|
-
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => operations.promise(signal, async (ownedSignal) => {
|
|
1060
|
-
// The same prelude a turn runs, and for the same reasons: a lapsed
|
|
1061
|
-
// OAuth token has to be renewed before the provider is used, and the
|
|
1062
|
-
// fallback chain has to be built AFTER that so its members do not
|
|
1063
|
-
// hold a client the refresh just replaced.
|
|
1064
|
-
await prepareProviderCredential(ownedSignal);
|
|
1065
|
-
const pluginSkills = pluginRuntime
|
|
1066
|
-
? await currentPluginSkills(pluginRuntime.skills)
|
|
1067
|
-
: undefined;
|
|
1068
|
-
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
1069
|
-
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
1070
|
-
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
|
|
1071
|
-
.filter((s) => Boolean(s))
|
|
1072
|
-
.join('\n\n') || undefined;
|
|
1073
|
-
return resumeRun({
|
|
1074
|
-
provider,
|
|
1075
|
-
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1076
|
-
tools: registry,
|
|
1077
|
-
pluginManager: pluginRuntime?.manager,
|
|
1078
|
-
skillRegistry: pluginRuntime?.skills,
|
|
1079
|
-
skills: pluginSkills,
|
|
1080
|
-
taskStore,
|
|
1081
|
-
...(subagentGateway ? { taskGateway: subagentGateway } : {}),
|
|
1082
|
-
authorizationGate: gateFor(options.rules),
|
|
1083
|
-
compactionConfig: COMPACTION_CONFIG,
|
|
1084
|
-
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1085
|
-
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1086
|
-
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1087
|
-
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
1088
|
-
: {}),
|
|
1089
|
-
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
1090
|
-
// whose `attach` detaches whoever held it before, so a caller
|
|
1091
|
-
// resuming several runs in one process would leave only the last
|
|
1092
|
-
// one covered — and would look covered. A turn owns its process
|
|
1093
|
-
// end to end; a drainer does not.
|
|
1094
|
-
runConfig: {
|
|
1095
|
-
model,
|
|
1096
|
-
timeoutMs: 600_000,
|
|
1097
|
-
tokenBudget: 1_000_000,
|
|
1098
|
-
maxIterations: 50,
|
|
1099
|
-
maxResponseTokens: 8192,
|
|
1100
|
-
permissionMode: 'auto',
|
|
1101
|
-
},
|
|
1102
|
-
agentId: 'namzu',
|
|
1103
|
-
agentName: 'namzu',
|
|
1104
|
-
...(systemPrompt ? { systemPrompt } : {}),
|
|
1105
|
-
workingDirectory: cwd,
|
|
1106
|
-
// No `onPermission`: there is nobody at a drainer's terminal, so a
|
|
1107
|
-
// prompt would block the pass forever on a run nobody is watching.
|
|
1108
|
-
// The gate's deny rules still apply.
|
|
1109
|
-
// One presenter for the whole stream, built from the registry this
|
|
1110
|
-
// scope already holds. It was the absence of the registry HERE that
|
|
1111
|
-
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
1112
|
-
// over a `RunEvent` and could not ask a tool anything.
|
|
1113
|
-
resumeHandler: makeResumeHandler(approval, undefined, options.permissionMode, (n, i) => isPromptExempt(registry, n, i)),
|
|
1114
|
-
signal: ownedSignal,
|
|
1115
|
-
// Attribution comes from the ENTRY, not from this session: the run
|
|
1116
|
-
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
1117
|
-
// it would file another tenant's work under this one.
|
|
1118
|
-
tenantId: entry.tenantId,
|
|
1119
|
-
projectId: entry.projectId,
|
|
1120
|
-
sessionId: entry.sessionId,
|
|
1121
|
-
// …except the topic, which no checkpoint records — see
|
|
1122
|
-
// `RunStateScope`. This one is the drainer's, and honestly so:
|
|
1123
|
-
// supplied here rather than pretended to have been recovered.
|
|
1124
|
-
topicId: scope.topicId,
|
|
1125
|
-
scope: { ...entry, topicId: scope.topicId },
|
|
1126
|
-
checkpointStore,
|
|
1127
|
-
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1128
|
-
});
|
|
1129
1187
|
}),
|
|
1130
1188
|
};
|
|
1131
1189
|
}
|
|
@@ -1485,7 +1543,7 @@ const COMPACTION_CONFIG = {
|
|
|
1485
1543
|
maxCharsPerRequirement: 300,
|
|
1486
1544
|
maxCharsPerTask: 400,
|
|
1487
1545
|
};
|
|
1488
|
-
async function* runTurn({ provider, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, workingDirectory,
|
|
1546
|
+
async function* runTurn({ provider, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, workingDirectory, sandboxWorkspace, rules, reviewAnswer, maxAnswerReviews, promoteMemory, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
|
|
1489
1547
|
const signal = opts?.signal;
|
|
1490
1548
|
// One presenter for the whole stream, built from the registry this scope
|
|
1491
1549
|
// already holds. Its absence HERE is what forced presentation to be name
|
|
@@ -1523,6 +1581,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1523
1581
|
emergencySave: true,
|
|
1524
1582
|
runConfig: {
|
|
1525
1583
|
model,
|
|
1584
|
+
...(sandboxProvider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1526
1585
|
...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
|
|
1527
1586
|
timeoutMs: 600_000,
|
|
1528
1587
|
tokenBudget: 1_000_000,
|
|
@@ -1550,7 +1609,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1550
1609
|
// The exemption reads `tools` at decision time, so it sees the task
|
|
1551
1610
|
// tools `query()` registers deferred below and any tool server that
|
|
1552
1611
|
// connected after this session was built.
|
|
1553
|
-
resumeHandler
|
|
1612
|
+
resumeHandler,
|
|
1554
1613
|
signal,
|
|
1555
1614
|
...scope,
|
|
1556
1615
|
});
|
|
@@ -1581,22 +1640,6 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1581
1640
|
const mapped = toAgentEvent(event, presenter);
|
|
1582
1641
|
if (!mapped)
|
|
1583
1642
|
continue;
|
|
1584
|
-
// On an `Agent` delegation finishing, attach the sub-agent's tool
|
|
1585
|
-
// steps (collected via the gateway while the call blocked) as a
|
|
1586
|
-
// `├─/└─` tree under its result, then reset for the next delegation.
|
|
1587
|
-
// (Don't clear on tool_executing: the parent's events can be buffered
|
|
1588
|
-
// and pulled only after the child already ran, which would wipe it.)
|
|
1589
|
-
if (event.type === 'tool_completed' &&
|
|
1590
|
-
event.toolName === 'Agent' &&
|
|
1591
|
-
childSteps.length > 0 &&
|
|
1592
|
-
mapped.kind === 'tool-end') {
|
|
1593
|
-
yield {
|
|
1594
|
-
...mapped,
|
|
1595
|
-
detail: [...(mapped.detail ?? []), ...asTree(childSteps)],
|
|
1596
|
-
};
|
|
1597
|
-
childSteps.length = 0;
|
|
1598
|
-
continue;
|
|
1599
|
-
}
|
|
1600
1643
|
yield mapped;
|
|
1601
1644
|
}
|
|
1602
1645
|
}
|
|
@@ -2014,10 +2057,6 @@ function describeFallback(event) {
|
|
|
2014
2057
|
`${status} ${why} (${event.code}). ` +
|
|
2015
2058
|
`${name(event.toIndex, event.toProviderId, event.toModel)} — is serving the rest of this turn.`);
|
|
2016
2059
|
}
|
|
2017
|
-
/** Render a sub-agent's tool steps as a `├─/└─` tree for the Agent result. */
|
|
2018
|
-
function asTree(steps) {
|
|
2019
|
-
return steps.map((s, i) => `${i === steps.length - 1 ? '└─' : '├─'} ${s}`);
|
|
2020
|
-
}
|
|
2021
2060
|
/**
|
|
2022
2061
|
* The one presentation function this host keeps.
|
|
2023
2062
|
*
|