@namzu/cli 15.1.1 → 16.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- 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/doctor/checks/chain.d.ts.map +1 -1
- package/dist/doctor/checks/chain.js +1 -0
- package/dist/doctor/checks/chain.js.map +1 -1
- package/dist/integrations/subagents/activity.d.ts +70 -0
- package/dist/integrations/subagents/activity.d.ts.map +1 -0
- package/dist/integrations/subagents/activity.js +332 -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 +27 -2
- package/dist/integrations/subagents/runtime.js.map +1 -1
- package/dist/tui/AgentExplorer.d.ts +36 -0
- package/dist/tui/AgentExplorer.d.ts.map +1 -0
- package/dist/tui/AgentExplorer.js +174 -0
- package/dist/tui/AgentExplorer.js.map +1 -0
- package/dist/tui/App.d.ts.map +1 -1
- package/dist/tui/App.js +269 -106
- 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 +199 -176
- 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/package.json +4 -4
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';
|
|
@@ -350,7 +351,24 @@ async function currentPluginSkills(registry) {
|
|
|
350
351
|
}
|
|
351
352
|
export async function createAgentSession(prefs, detected, options = {}) {
|
|
352
353
|
const scope = options.scope ?? mintScope();
|
|
353
|
-
const
|
|
354
|
+
const requestedCwd = resolve(options.cwd ?? process.cwd());
|
|
355
|
+
let cwd;
|
|
356
|
+
try {
|
|
357
|
+
const entry = await stat(requestedCwd);
|
|
358
|
+
if (!entry.isDirectory()) {
|
|
359
|
+
return emptySession(`Working directory is not a directory: ${requestedCwd}`, 'invocation');
|
|
360
|
+
}
|
|
361
|
+
cwd = await realpath(requestedCwd);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
return emptySession(`Working directory is unavailable: ${requestedCwd} — ${describeError(error)}`, 'invocation');
|
|
365
|
+
}
|
|
366
|
+
const sandboxWorkspace = options.sandbox?.workspace ?? 'working-directory';
|
|
367
|
+
if (options.sandbox?.enabled !== false &&
|
|
368
|
+
sandboxWorkspace === 'working-directory' &&
|
|
369
|
+
cwd === parse(cwd).root) {
|
|
370
|
+
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');
|
|
371
|
+
}
|
|
354
372
|
// The head serves; the tail is fallen over to, in order, when it cannot.
|
|
355
373
|
const primary = primaryProvider(prefs);
|
|
356
374
|
const entry = PROVIDER_REGISTRY[primary.id];
|
|
@@ -662,6 +680,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
662
680
|
// `buildToolRegistry` directly below, so they never receive these tools.
|
|
663
681
|
// Per-send denial further keeps the schemas out of ordinary human turns.
|
|
664
682
|
const goalAuthorities = new Map();
|
|
683
|
+
// A child is created after the parent query has started, from inside its
|
|
684
|
+
// Agent tool. Keying the review channel by the executing run keeps two
|
|
685
|
+
// concurrent sends from borrowing each other's prompt or approval latch.
|
|
686
|
+
const delegatedResumeHandlers = new Map();
|
|
665
687
|
const goalToolNames = new Set(SESSION_GOAL_TOOL_NAMES);
|
|
666
688
|
if (options.sessionGoals) {
|
|
667
689
|
registry.register(buildSessionGoalTools(options.sessionGoals, (runId) => goalAuthorities.get(runId)));
|
|
@@ -714,11 +736,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
714
736
|
// Best-effort — if the runtime can't stand up, the chat still works.
|
|
715
737
|
let subagentGateway;
|
|
716
738
|
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
739
|
// Stays empty when the runtime below throws, which is the honest answer: the
|
|
723
740
|
// catch is non-fatal and the session then genuinely has no delegate to
|
|
724
741
|
// dispatch to. A roster reported from the request rather than the result
|
|
@@ -728,6 +745,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
728
745
|
const sub = await createSubagentRuntime({
|
|
729
746
|
cwd,
|
|
730
747
|
model,
|
|
748
|
+
sandboxWorkspace,
|
|
749
|
+
resolveResumeHandler: (runId) => delegatedResumeHandlers.get(runId),
|
|
731
750
|
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
732
751
|
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
733
752
|
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
@@ -769,11 +788,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
769
788
|
return buildToolRegistry(cwd).registry;
|
|
770
789
|
},
|
|
771
790
|
authorizationGate: gateFor(options.rules),
|
|
772
|
-
onEvent: (e) => {
|
|
773
|
-
if (e.type === 'tool_executing') {
|
|
774
|
-
childSteps.push(`${e.toolName}(${genericLabel(e.input)})`);
|
|
775
|
-
}
|
|
776
|
-
},
|
|
777
791
|
});
|
|
778
792
|
subagentRuntime = sub;
|
|
779
793
|
registry.register([sub.agentTool]);
|
|
@@ -905,6 +919,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
905
919
|
...(sandbox.environment ? { environment: sandbox.environment } : {}),
|
|
906
920
|
enforced: sandbox.enforced,
|
|
907
921
|
required: sandbox.required,
|
|
922
|
+
workspace: sandbox.workspace,
|
|
908
923
|
},
|
|
909
924
|
providerSummary: entry.label,
|
|
910
925
|
modelSummary: model,
|
|
@@ -928,6 +943,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
928
943
|
.map((t) => t.name)
|
|
929
944
|
.filter((name) => !goalToolNames.has(name)),
|
|
930
945
|
agentIds: allowedAgentIds,
|
|
946
|
+
...(subagentRuntime ? { subagents: subagentRuntime.activity } : {}),
|
|
931
947
|
get instructionFiles() {
|
|
932
948
|
return projectInstructions.instructionFiles;
|
|
933
949
|
},
|
|
@@ -960,172 +976,198 @@ export async function createAgentSession(prefs, detected, options = {}) {
|
|
|
960
976
|
},
|
|
961
977
|
promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
|
|
962
978
|
send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
979
|
+
const runId = opts?.runId ?? generateRunId();
|
|
980
|
+
const turnOpts = { ...opts, runId, signal };
|
|
981
|
+
const resumeHandler = makeResumeHandler(approval, opts?.onPermission, opts?.permissionMode ?? options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
982
|
+
if (delegatedResumeHandlers.has(runId)) {
|
|
983
|
+
throw new Error(`Run ${runId} already owns a delegated review channel.`);
|
|
984
|
+
}
|
|
985
|
+
delegatedResumeHandlers.set(runId, resumeHandler);
|
|
986
|
+
try {
|
|
987
|
+
// Renew a lapsed OAuth token before the turn runs (no-op for valid
|
|
988
|
+
// tokens and non-subscription credentials).
|
|
989
|
+
await prepareProviderCredential(signal);
|
|
990
|
+
// namzu identity first (so it establishes who the agent is even when
|
|
991
|
+
// the credential layer prepends whatever prefix its token requires),
|
|
992
|
+
// then memory and per-turn extra context. Project instructions are a
|
|
993
|
+
// separate retained user-context snapshot prepared by the controller;
|
|
994
|
+
// that is what lets nested scopes be replaced and persisted safely.
|
|
995
|
+
//
|
|
996
|
+
// The environment block is read fresh every turn because both facts in it
|
|
997
|
+
// can change WHILE the session
|
|
998
|
+
// runs — midnight passes, and the agent checks out a branch itself.
|
|
999
|
+
// Its text only changes when a fact changes, so it costs a prompt-cache
|
|
1000
|
+
// miss exactly when a hit would have been a stale claim.
|
|
1001
|
+
const pluginSkills = pluginRuntime
|
|
1002
|
+
? await currentPluginSkills(pluginRuntime.skills)
|
|
1003
|
+
: undefined;
|
|
1004
|
+
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
1005
|
+
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
1006
|
+
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt, opts?.extraSystem]
|
|
1007
|
+
.filter((s) => Boolean(s))
|
|
1008
|
+
.join('\n\n') || undefined;
|
|
1009
|
+
let capturedAuthority;
|
|
1010
|
+
if (opts?.goalRound) {
|
|
1011
|
+
if (!opts.runId)
|
|
1012
|
+
throw new Error('A goal round requires a caller-reserved runId.');
|
|
1013
|
+
if (!options.sessionGoals)
|
|
1014
|
+
throw new Error('This session has no durable goal store.');
|
|
1015
|
+
if (opts.goalRound.sessionId !== scope.sessionId ||
|
|
1016
|
+
opts.goalRound.tenantId !== scope.tenantId) {
|
|
1017
|
+
throw new Error('Goal-round authority does not belong to this agent session scope.');
|
|
1018
|
+
}
|
|
1019
|
+
const current = await options.sessionGoals.getGoal(scope.sessionId, scope.tenantId);
|
|
1020
|
+
if (!current ||
|
|
1021
|
+
current.phase !== 'active' ||
|
|
1022
|
+
current.id !== opts.goalRound.id ||
|
|
1023
|
+
current.revision !== opts.goalRound.revision ||
|
|
1024
|
+
current.objective !== opts.goalRound.objective ||
|
|
1025
|
+
current.roundsAdmitted !== opts.goalRound.round ||
|
|
1026
|
+
current.maxGoalRounds !== opts.goalRound.maxGoalRounds) {
|
|
1027
|
+
throw new Error('Goal-round authority is stale or does not match the durable goal.');
|
|
1028
|
+
}
|
|
1029
|
+
if (goalAuthorities.has(opts.runId)) {
|
|
1030
|
+
throw new Error(`Run ${opts.runId} already owns goal-round authority.`);
|
|
1031
|
+
}
|
|
1032
|
+
capturedAuthority = Object.freeze({ ...opts.goalRound });
|
|
1033
|
+
goalAuthorities.set(opts.runId, capturedAuthority);
|
|
1034
|
+
}
|
|
1035
|
+
try {
|
|
1036
|
+
yield* runTurn({
|
|
1037
|
+
provider,
|
|
1038
|
+
// Constructed HERE, per turn, and that is not an optimisation to
|
|
1039
|
+
// undo. `refreshTokenIfNeeded` above replaces the head's client
|
|
1040
|
+
// object when an OAuth token rotates, so a member list built once at
|
|
1041
|
+
// session creation would hand the kernel a client holding a token
|
|
1042
|
+
// that expired hours ago — and a chain whose own members are stale
|
|
1043
|
+
// is a fallback that fails for the reason the fallback exists to
|
|
1044
|
+
// survive. Building a driver is a client object, not a request.
|
|
1045
|
+
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1046
|
+
model,
|
|
1047
|
+
tools: registry,
|
|
1048
|
+
pluginManager: pluginRuntime?.manager,
|
|
1049
|
+
skillRegistry: pluginRuntime?.skills,
|
|
1050
|
+
skills: pluginSkills,
|
|
1051
|
+
scope,
|
|
1052
|
+
workingDirectory: cwd,
|
|
1053
|
+
sandboxWorkspace,
|
|
1054
|
+
rules: options.rules,
|
|
1055
|
+
reviewAnswer: options.reviewAnswer,
|
|
1056
|
+
maxAnswerReviews: options.maxAnswerReviews,
|
|
1057
|
+
promoteMemory,
|
|
1058
|
+
taskStore,
|
|
1059
|
+
systemPrompt,
|
|
1060
|
+
messages,
|
|
1061
|
+
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1062
|
+
opts: turnOpts,
|
|
1063
|
+
resumeHandler,
|
|
1064
|
+
taskGateway: subagentGateway,
|
|
1065
|
+
onRunEvent: options.onRunEvent,
|
|
1066
|
+
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1067
|
+
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1068
|
+
? {
|
|
1069
|
+
sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs,
|
|
1070
|
+
}
|
|
1071
|
+
: {}),
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
finally {
|
|
1075
|
+
if (opts?.runId &&
|
|
1076
|
+
capturedAuthority &&
|
|
1077
|
+
goalAuthorities.get(opts.runId) === capturedAuthority) {
|
|
1078
|
+
goalAuthorities.delete(opts.runId);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
finally {
|
|
1083
|
+
if (delegatedResumeHandlers.get(runId) === resumeHandler) {
|
|
1084
|
+
delegatedResumeHandlers.delete(runId);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
})()),
|
|
1088
|
+
resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => operations.promise(signal, async (ownedSignal) => {
|
|
1089
|
+
// The same prelude a turn runs, and for the same reasons: a lapsed
|
|
1090
|
+
// OAuth token has to be renewed before the provider is used, and the
|
|
1091
|
+
// fallback chain has to be built AFTER that so its members do not
|
|
1092
|
+
// hold a client the refresh just replaced.
|
|
1093
|
+
await prepareProviderCredential(ownedSignal);
|
|
977
1094
|
const pluginSkills = pluginRuntime
|
|
978
1095
|
? await currentPluginSkills(pluginRuntime.skills)
|
|
979
1096
|
: undefined;
|
|
980
1097
|
const memoryPrompt = composeMemoryPrompt(readMemory());
|
|
981
1098
|
const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
|
|
982
|
-
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt
|
|
1099
|
+
const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
|
|
983
1100
|
.filter((s) => Boolean(s))
|
|
984
1101
|
.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);
|
|
1102
|
+
const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
|
|
1103
|
+
if (delegatedResumeHandlers.has(entry.runId)) {
|
|
1104
|
+
throw new Error(`Run ${entry.runId} already owns a delegated review channel.`);
|
|
1010
1105
|
}
|
|
1106
|
+
delegatedResumeHandlers.set(entry.runId, resumeHandler);
|
|
1011
1107
|
try {
|
|
1012
|
-
|
|
1108
|
+
return await resumeRun({
|
|
1013
1109
|
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
1110
|
fallbackProviders: fallbackPlan.build(currentToken),
|
|
1022
|
-
model,
|
|
1023
1111
|
tools: registry,
|
|
1024
1112
|
pluginManager: pluginRuntime?.manager,
|
|
1025
1113
|
skillRegistry: pluginRuntime?.skills,
|
|
1026
1114
|
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
1115
|
taskStore,
|
|
1036
|
-
|
|
1037
|
-
|
|
1116
|
+
...(subagentGateway ? { taskGateway: subagentGateway } : {}),
|
|
1117
|
+
authorizationGate: gateFor(options.rules),
|
|
1118
|
+
compactionConfig: COMPACTION_CONFIG,
|
|
1038
1119
|
projectInstructionContext: projectInstructions.createRunContext(),
|
|
1039
|
-
opts: { ...opts, signal },
|
|
1040
|
-
taskGateway: subagentGateway,
|
|
1041
|
-
onRunEvent: options.onRunEvent,
|
|
1042
|
-
childSteps,
|
|
1043
1120
|
...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
|
|
1044
1121
|
...(options.sandbox?.teardownTimeoutMs !== undefined
|
|
1045
|
-
? {
|
|
1046
|
-
sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs,
|
|
1047
|
-
}
|
|
1122
|
+
? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
|
|
1048
1123
|
: {}),
|
|
1124
|
+
// NOT `emergencySave`, unlike a turn. The manager is a singleton
|
|
1125
|
+
// whose `attach` detaches whoever held it before, so a caller
|
|
1126
|
+
// resuming several runs in one process would leave only the last
|
|
1127
|
+
// one covered — and would look covered. A turn owns its process
|
|
1128
|
+
// end to end; a drainer does not.
|
|
1129
|
+
runConfig: {
|
|
1130
|
+
model,
|
|
1131
|
+
...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1132
|
+
timeoutMs: 600_000,
|
|
1133
|
+
tokenBudget: 1_000_000,
|
|
1134
|
+
maxIterations: 50,
|
|
1135
|
+
maxResponseTokens: 8192,
|
|
1136
|
+
permissionMode: 'auto',
|
|
1137
|
+
},
|
|
1138
|
+
agentId: 'namzu',
|
|
1139
|
+
agentName: 'namzu',
|
|
1140
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
1141
|
+
workingDirectory: cwd,
|
|
1142
|
+
// No `onPermission`: there is nobody at a drainer's terminal, so a
|
|
1143
|
+
// prompt would block the pass forever on a run nobody is watching.
|
|
1144
|
+
// The gate's deny rules still apply.
|
|
1145
|
+
// One presenter for the whole stream, built from the registry this
|
|
1146
|
+
// scope already holds. It was the absence of the registry HERE that
|
|
1147
|
+
// forced presentation to be name matching: `toAgentEvent` was pure
|
|
1148
|
+
// over a `RunEvent` and could not ask a tool anything.
|
|
1149
|
+
resumeHandler,
|
|
1150
|
+
signal: ownedSignal,
|
|
1151
|
+
// Attribution comes from the ENTRY, not from this session: the run
|
|
1152
|
+
// belongs to whoever started it, and stamping the drainer's ids onto
|
|
1153
|
+
// it would file another tenant's work under this one.
|
|
1154
|
+
tenantId: entry.tenantId,
|
|
1155
|
+
projectId: entry.projectId,
|
|
1156
|
+
sessionId: entry.sessionId,
|
|
1157
|
+
// …except the topic, which no checkpoint records — see
|
|
1158
|
+
// `RunStateScope`. This one is the drainer's, and honestly so:
|
|
1159
|
+
// supplied here rather than pretended to have been recovered.
|
|
1160
|
+
topicId: scope.topicId,
|
|
1161
|
+
scope: { ...entry, topicId: scope.topicId },
|
|
1162
|
+
checkpointStore,
|
|
1163
|
+
...(claimFence !== undefined ? { claimFence } : {}),
|
|
1049
1164
|
});
|
|
1050
1165
|
}
|
|
1051
1166
|
finally {
|
|
1052
|
-
if (
|
|
1053
|
-
|
|
1054
|
-
goalAuthorities.get(opts.runId) === capturedAuthority) {
|
|
1055
|
-
goalAuthorities.delete(opts.runId);
|
|
1167
|
+
if (delegatedResumeHandlers.get(entry.runId) === resumeHandler) {
|
|
1168
|
+
delegatedResumeHandlers.delete(entry.runId);
|
|
1056
1169
|
}
|
|
1057
1170
|
}
|
|
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
1171
|
}),
|
|
1130
1172
|
};
|
|
1131
1173
|
}
|
|
@@ -1485,7 +1527,7 @@ const COMPACTION_CONFIG = {
|
|
|
1485
1527
|
maxCharsPerRequirement: 300,
|
|
1486
1528
|
maxCharsPerTask: 400,
|
|
1487
1529
|
};
|
|
1488
|
-
async function* runTurn({ provider, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, workingDirectory,
|
|
1530
|
+
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
1531
|
const signal = opts?.signal;
|
|
1490
1532
|
// One presenter for the whole stream, built from the registry this scope
|
|
1491
1533
|
// already holds. Its absence HERE is what forced presentation to be name
|
|
@@ -1523,6 +1565,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1523
1565
|
emergencySave: true,
|
|
1524
1566
|
runConfig: {
|
|
1525
1567
|
model,
|
|
1568
|
+
...(sandboxProvider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
|
|
1526
1569
|
...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
|
|
1527
1570
|
timeoutMs: 600_000,
|
|
1528
1571
|
tokenBudget: 1_000_000,
|
|
@@ -1550,7 +1593,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1550
1593
|
// The exemption reads `tools` at decision time, so it sees the task
|
|
1551
1594
|
// tools `query()` registers deferred below and any tool server that
|
|
1552
1595
|
// connected after this session was built.
|
|
1553
|
-
resumeHandler
|
|
1596
|
+
resumeHandler,
|
|
1554
1597
|
signal,
|
|
1555
1598
|
...scope,
|
|
1556
1599
|
});
|
|
@@ -1581,22 +1624,6 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
|
|
|
1581
1624
|
const mapped = toAgentEvent(event, presenter);
|
|
1582
1625
|
if (!mapped)
|
|
1583
1626
|
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
1627
|
yield mapped;
|
|
1601
1628
|
}
|
|
1602
1629
|
}
|
|
@@ -2014,10 +2041,6 @@ function describeFallback(event) {
|
|
|
2014
2041
|
`${status} ${why} (${event.code}). ` +
|
|
2015
2042
|
`${name(event.toIndex, event.toProviderId, event.toModel)} — is serving the rest of this turn.`);
|
|
2016
2043
|
}
|
|
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
2044
|
/**
|
|
2022
2045
|
* The one presentation function this host keeps.
|
|
2023
2046
|
*
|