@xingular/harness 1.1.1 → 1.2.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 +13 -0
- package/README.md +28 -25
- package/dist/cli.js +6675 -2501
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.js +15 -1
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +2 -1
- package/dist/sdk/agents/profiles.js +9 -4
- package/dist/sdk/annotations/pageScript.js +349 -48
- package/dist/sdk/annotations/part.js +34 -23
- package/dist/sdk/annotations/store.d.ts +7 -0
- package/dist/sdk/annotations/store.js +13 -5
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +5 -4
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +250 -0
- package/dist/sdk/commands/registry.js +47 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
- package/dist/sdk/core/workspaceBashTool.js +36 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
- package/dist/sdk/core/workspaceSandbox.js +320 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
- package/dist/sdk/core/workspaceWriteOperations.js +206 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +7 -0
- package/dist/sdk/harness.js +42 -2
- package/dist/sdk/index.d.ts +6 -1
- package/dist/sdk/index.js +3 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +29 -12
- package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
- package/dist/sdk/plugins/builtinRegistry.js +30 -8
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/inventory.js +4 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +14 -0
- package/dist/sdk/prompt/compile.js +65 -29
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +18 -0
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +3 -0
- package/dist/sdk/sessions/continuation.js +5 -1
- package/dist/sdk/sessions/contracts.d.ts +168 -0
- package/dist/sdk/sessions/contracts.js +59 -1
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +14 -3
- package/dist/sdk/sessions/planMode.js +73 -16
- package/dist/sdk/sessions/registry.d.ts +1 -1
- package/dist/sdk/sessions/registry.js +2 -2
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +35 -0
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +161 -13
- package/dist/sdk/sessions/sessions.d.ts +23 -3
- package/dist/sdk/sessions/sessions.js +202 -17
- package/dist/sdk/sessions/subagentService.d.ts +9 -0
- package/dist/sdk/sessions/subagentService.js +63 -4
- package/dist/sdk/sessions/workSlice.d.ts +103 -0
- package/dist/sdk/sessions/workSlice.js +297 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browserControl.js +2 -1
- package/dist/sdk/terminal/controller.d.ts +11 -3
- package/dist/sdk/terminal/controller.js +40 -13
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +87 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +154 -54
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
- package/dist/sdk/terminal/subagentMonitor.js +32 -1
- package/dist/sdk/terminal/terminals.d.ts +19 -2
- package/dist/sdk/terminal/terminals.js +245 -35
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +205 -124
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
- package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
- package/dist/sdk/terminal/tui/runtime.js +23 -3
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/tools/checklist.js +1 -0
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/spawnSubagent.js +4 -3
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/terminal-opentui.js +5207 -1883
- package/dist/terminal-pi.js +1658 -271
- package/docs/architecture/02-sessions-and-runs.md +128 -26
- package/docs/architecture/04-prompt.md +50 -12
- package/docs/architecture/06-plugins-and-capabilities.md +24 -7
- package/docs/architecture/07-tools-and-gate.md +104 -24
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +31 -1
- package/docs/architecture/10-server-protocol.md +58 -3
- package/docs/architecture/11-terminal.md +105 -42
- package/docs/architecture/12-storage.md +30 -2
- package/docs/architecture/13-world-tools.md +22 -1
- package/docs/architecture/14-surfaces-and-parity.md +54 -7
- package/docs/architecture/README.md +3 -1
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- package/docs/architecture/proposals/README.md +14 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +80 -24
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/4.0.0.md +10 -9
- package/docs/releases/README.md +35 -30
- package/docs/study/00-index.md +3 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/terminal-tui.md +100 -59
- package/package.json +4 -3
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
// Starting a session turn: the account, the run record, the compiled prompt, the tools and
|
|
2
2
|
// their gate, then Pi. Everything before Pi answers is synchronous with the caller; the reply
|
|
3
3
|
// itself streams as run events.
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
+
import { isAbsolute, join } from 'node:path';
|
|
6
7
|
import { log } from "../core/bus.js";
|
|
7
8
|
import { HarnessError, invalid } from "../core/errors.js";
|
|
9
|
+
import { formatBashCommandForApproval } from "../core/workspaceBashTool.js";
|
|
8
10
|
import { capabilitySet as parseCapabilitySet, freezeRun } from "../capabilities/runSnapshot.js";
|
|
9
11
|
import { now, writeJson } from "../core/fs.js";
|
|
12
|
+
import { resolveWorkspaceRoot, verifyWorkspaceRoot } from "../core/workspace.js";
|
|
10
13
|
import { takeChars } from "../core/text.js";
|
|
11
14
|
import { BUILTIN_PLUGIN_REGISTRY } from "../plugins/builtin.js";
|
|
12
15
|
import { sessionLaunch } from "../prompt/compile.js";
|
|
@@ -16,17 +19,43 @@ import { imagePruning } from "../world/images.js";
|
|
|
16
19
|
import { materialise } from "../variant/materialise.js";
|
|
17
20
|
import { ASK_USER } from "../tools/askUser.js";
|
|
18
21
|
import { CHECKLIST_WRITE } from "../tools/checklist.js";
|
|
22
|
+
import { CHECKPOINT_WRITE, checkpointOf } from "../tools/checkpoint.js";
|
|
19
23
|
import { SESSION_NAME, sessionNameTool } from "../tools/sessionName.js";
|
|
20
24
|
import { DEFAULT_TOOL_NAMES } from "../tools/definitions.js";
|
|
25
|
+
import { GET_CAPABILITIES, getCapabilitiesTool } from "../tools/getCapabilities.js";
|
|
26
|
+
import { isActiveStatus } from "./contracts.js";
|
|
21
27
|
import { SPAWN_SUBAGENT, WAIT_SUBAGENTS, subagentDepth, subagentHistory, subagentNesting } from "./subagents.js";
|
|
22
28
|
import { selectHistory } from "./history.js";
|
|
23
29
|
import { runLineageJoin } from "./lineageJoin.js";
|
|
30
|
+
import { compactionCheckpoint, COMPACTION_CHECKPOINT_EXTENSION } from "./compactionCheckpoint.js";
|
|
31
|
+
import { createSliceState, parseWorkSlice, sliceOf, workSliceGuard, WorkSliceError, WORK_SLICE_EXTENSION } from "./workSlice.js";
|
|
24
32
|
import { piSessionFactory, runTurn, summarize } from "./runner.js";
|
|
25
|
-
import { loadRun, runDir, saveRun, updateRun } from "./runStore.js";
|
|
26
|
-
import {
|
|
33
|
+
import { loadRun, readEvents, runDir, saveRun, updateRun } from "./runStore.js";
|
|
34
|
+
import { combineWorkspaceScope, inspectRunWorkspaceWrite } from "./workspaceBoundary.js";
|
|
35
|
+
import { PLAN_INSTRUCTIONS, PLAN_RESEARCH_SUBAGENT_INSTRUCTIONS, planRefusal, planSection } from "./planMode.js";
|
|
27
36
|
import { CONTINUATION_SECTION_ID, continuationOf, continuationSection } from "./continuation.js";
|
|
28
37
|
import { deliveredSnapshot } from "../prompt/compile.js";
|
|
29
38
|
import { profileSection } from "../agents/profiles.js";
|
|
39
|
+
/** A session command acts on a saved conversation and is never combined with a request of its own. */
|
|
40
|
+
function checkCommand(command, historyFile, input) {
|
|
41
|
+
if (!historyFile)
|
|
42
|
+
throw invalid('There is no saved conversation for this command yet.');
|
|
43
|
+
if (input.plan || input.continueFrom || input.automatic || input.initialBashCommand != null || input.images?.length) {
|
|
44
|
+
throw invalid('A session command runs on its own, without plan mode, a continuation, Bash or images.');
|
|
45
|
+
}
|
|
46
|
+
switch (command?.name) {
|
|
47
|
+
case 'compact': return;
|
|
48
|
+
case 'navigate':
|
|
49
|
+
if (typeof command.entryId !== 'string' || !command.entryId)
|
|
50
|
+
throw invalid('navigate needs the entry to go to.');
|
|
51
|
+
return;
|
|
52
|
+
case 'export':
|
|
53
|
+
if (typeof command.path !== 'string' || !isAbsolute(command.path) || !command.path.endsWith('.html'))
|
|
54
|
+
throw invalid('export needs an absolute .html path.');
|
|
55
|
+
return;
|
|
56
|
+
default: throw invalid('Unknown session command.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
30
59
|
const CONTINUE_TEXT = 'Continue from where you stopped.';
|
|
31
60
|
/** Fills a continuing run's input from the run it continues, refusing anything that would differ. */
|
|
32
61
|
function continueInput(root, input) {
|
|
@@ -95,7 +124,7 @@ export class Sessions {
|
|
|
95
124
|
diagnosticGate(run, caller) {
|
|
96
125
|
return this.gate(run, caller, new Set(), true);
|
|
97
126
|
}
|
|
98
|
-
gate(run, caller, gated, diagnostic = false) {
|
|
127
|
+
gate(run, caller, gated, diagnostic = false, automatic = false) {
|
|
99
128
|
const { policy } = this.options;
|
|
100
129
|
return {
|
|
101
130
|
ask: (names) => {
|
|
@@ -103,9 +132,19 @@ export class Sessions {
|
|
|
103
132
|
gated.add(name);
|
|
104
133
|
},
|
|
105
134
|
check: async (tool, args) => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
135
|
+
let record;
|
|
136
|
+
let inspection;
|
|
137
|
+
try {
|
|
138
|
+
record = loadRun(this.env.root, run.runId);
|
|
139
|
+
inspection = inspectRunWorkspaceWrite(record, tool, args);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return error instanceof Error ? error.message : 'The active project workspace could not be verified.';
|
|
143
|
+
}
|
|
144
|
+
const call = { run: record, caller, tool, args: inspection.input };
|
|
145
|
+
const hostScope = (await policy.scope?.(call)) ?? { allowed: true };
|
|
146
|
+
const scope = combineWorkspaceScope(hostScope, inspection);
|
|
147
|
+
if (!scope.allowed && (scope.automatic || (automatic && inspection.outside) || !scope.requiresApproval))
|
|
109
148
|
return scope.reason ?? 'Outside the approved work scope.';
|
|
110
149
|
try {
|
|
111
150
|
await policy.authorize?.(call);
|
|
@@ -122,11 +161,11 @@ export class Sessions {
|
|
|
122
161
|
return scope.reason ? `Refused in a refinement run: ${scope.reason}` : 'Refused in a refinement run: this call needs a person to approve it.';
|
|
123
162
|
if (call.run.trigger === 'reflection')
|
|
124
163
|
return scope.reason ? `Refused in a reflection run: ${scope.reason}` : 'Refused in a reflection run: this call needs a person to approve it.';
|
|
125
|
-
|
|
164
|
+
const boundaryAction = inspection.outside ? `${inspection.reason} ${summarize(call.args, tool)}` : summarize(call.args);
|
|
126
165
|
const decision = await run.requestApproval({
|
|
127
166
|
target: tool,
|
|
128
|
-
action:
|
|
129
|
-
operation: { tool, args: (args && typeof args === 'object' ? args : {}) },
|
|
167
|
+
action: boundaryAction,
|
|
168
|
+
operation: { tool, args: (call.args && typeof call.args === 'object' ? call.args : {}) },
|
|
130
169
|
rememberable: scope.allowed && !scope.humanOnly,
|
|
131
170
|
standing: scope.humanOnly ? (scope.allowGlobalApproval ? 'switch' : 'none') : undefined
|
|
132
171
|
});
|
|
@@ -139,6 +178,69 @@ export class Sessions {
|
|
|
139
178
|
return error instanceof Error ? error.message : 'Tool was disabled while waiting.';
|
|
140
179
|
}
|
|
141
180
|
return null;
|
|
181
|
+
},
|
|
182
|
+
workspaceBoundaryBypass: async (tool, args) => {
|
|
183
|
+
if (diagnostic || automatic || tool.toLowerCase() !== 'bash')
|
|
184
|
+
return false;
|
|
185
|
+
const record = loadRun(this.env.root, run.runId);
|
|
186
|
+
if (record.trigger != null && record.trigger !== 'user')
|
|
187
|
+
return false;
|
|
188
|
+
if (!await this.options.fullPermission?.(record))
|
|
189
|
+
return false;
|
|
190
|
+
const call = { run: record, caller, tool, args };
|
|
191
|
+
const scope = (await policy.scope?.(call)) ?? { allowed: true };
|
|
192
|
+
if (scope.automatic || (!scope.allowed && !scope.requiresApproval) || (scope.humanOnly && !scope.allowGlobalApproval))
|
|
193
|
+
return false;
|
|
194
|
+
try {
|
|
195
|
+
await policy.authorize?.(call);
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
requestWorkspaceWriteScope: async (tool, args, reason) => {
|
|
203
|
+
if (diagnostic || automatic || tool.toLowerCase() !== 'bash')
|
|
204
|
+
return false;
|
|
205
|
+
const record = loadRun(this.env.root, run.runId);
|
|
206
|
+
if (!isActiveStatus(record.status) || (record.trigger != null && record.trigger !== 'user'))
|
|
207
|
+
return false;
|
|
208
|
+
const operationArgs = (args && typeof args === 'object' && !Array.isArray(args) ? args : {});
|
|
209
|
+
if (operationArgs.workspaceWriteScope !== 'outside' || typeof operationArgs.command !== 'string' || !operationArgs.command)
|
|
210
|
+
return false;
|
|
211
|
+
const call = { run: record, caller, tool, args: operationArgs };
|
|
212
|
+
const scope = (await policy.scope?.(call)) ?? { allowed: true };
|
|
213
|
+
if (scope.automatic || (!scope.allowed && !scope.requiresApproval))
|
|
214
|
+
return false;
|
|
215
|
+
try {
|
|
216
|
+
await policy.authorize?.(call);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
const decision = await run.requestApproval({
|
|
222
|
+
target: 'workspace-write',
|
|
223
|
+
action: `${reason}\n\nCommand to be run once after approval:\n${formatBashCommandForApproval(operationArgs.command)}`,
|
|
224
|
+
operation: { tool, args: operationArgs },
|
|
225
|
+
rememberable: false,
|
|
226
|
+
standing: 'none'
|
|
227
|
+
});
|
|
228
|
+
if (decision !== 'approved')
|
|
229
|
+
return false;
|
|
230
|
+
const currentRecord = loadRun(this.env.root, run.runId);
|
|
231
|
+
if (!isActiveStatus(currentRecord.status) || (currentRecord.trigger != null && currentRecord.trigger !== 'user'))
|
|
232
|
+
return false;
|
|
233
|
+
const currentCall = { run: currentRecord, caller, tool, args: operationArgs };
|
|
234
|
+
const currentScope = (await policy.scope?.(currentCall)) ?? { allowed: true };
|
|
235
|
+
if (currentScope.automatic || (!currentScope.allowed && !currentScope.requiresApproval))
|
|
236
|
+
return false;
|
|
237
|
+
try {
|
|
238
|
+
await policy.authorize?.(currentCall);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
142
244
|
}
|
|
143
245
|
};
|
|
144
246
|
}
|
|
@@ -195,6 +297,50 @@ export class Sessions {
|
|
|
195
297
|
});
|
|
196
298
|
return { release };
|
|
197
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Starts the next slice when a reply stopped at its work-slice boundary (PROP-0041). Never for
|
|
302
|
+
* unattended, plan, subagent, command or worktree runs, after a blocked or fruitless slice, or
|
|
303
|
+
* past the policy's slice cap. The earlier record is never reopened; this is a new run with the
|
|
304
|
+
* same pins, exactly as an application-asked continuation.
|
|
305
|
+
*/
|
|
306
|
+
async continueSlice(input) {
|
|
307
|
+
const { root, runId, source, policy } = input;
|
|
308
|
+
if (!policy.enabled || !policy.continue || policy.maxSlices <= 0)
|
|
309
|
+
return;
|
|
310
|
+
if (source.automatic || source.plan || source.command || source.run.relation || source.run.worktreeId)
|
|
311
|
+
return;
|
|
312
|
+
if ((source.depth ?? 0) > 0)
|
|
313
|
+
return;
|
|
314
|
+
const record = loadRun(root, runId);
|
|
315
|
+
if (record.terminalReason !== 'slice_complete')
|
|
316
|
+
return;
|
|
317
|
+
const events = readEvents(root, runId);
|
|
318
|
+
const checkpoint = checkpointOf(events);
|
|
319
|
+
if (checkpoint?.blocked)
|
|
320
|
+
return;
|
|
321
|
+
const progressed = events.some((event) => event.type === 'file.changed')
|
|
322
|
+
|| events.some((event) => {
|
|
323
|
+
const payload = event.payload;
|
|
324
|
+
return event.type === 'tool.completed' && payload.name === 'bash' && payload.ok === true;
|
|
325
|
+
})
|
|
326
|
+
|| (checkpoint?.done.length ?? 0) > 0;
|
|
327
|
+
if (!progressed)
|
|
328
|
+
return;
|
|
329
|
+
if (sliceOf(root, runId) >= policy.maxSlices)
|
|
330
|
+
return;
|
|
331
|
+
const next = {
|
|
332
|
+
...source,
|
|
333
|
+
run: { ...source.run, id: randomUUID(), resumedFromRunId: null, parentRunId: null, relation: null },
|
|
334
|
+
continueFrom: runId,
|
|
335
|
+
text: CONTINUE_TEXT
|
|
336
|
+
};
|
|
337
|
+
next.initialBashCommand = undefined;
|
|
338
|
+
next.initialBashContext = undefined;
|
|
339
|
+
next.images = undefined;
|
|
340
|
+
next.resume = null;
|
|
341
|
+
next.command = undefined;
|
|
342
|
+
await this.run(next).catch((error) => log(`the next work slice did not start: ${error instanceof Error ? error.message : String(error)}`));
|
|
343
|
+
}
|
|
198
344
|
async start(input) {
|
|
199
345
|
const { root } = this.env;
|
|
200
346
|
if ((input.initialBashCommand !== undefined && input.initialBashCommand !== null) || (input.initialBashContext !== undefined && input.initialBashContext !== null)) {
|
|
@@ -222,8 +368,23 @@ export class Sessions {
|
|
|
222
368
|
throw invalid('An initial Bash command is available only for a new interactive user turn.');
|
|
223
369
|
}
|
|
224
370
|
}
|
|
371
|
+
let slicePolicy;
|
|
372
|
+
try {
|
|
373
|
+
slicePolicy = parseWorkSlice(input.workSlice);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
if (error instanceof WorkSliceError)
|
|
377
|
+
throw invalid(error.message);
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
225
380
|
const continuation = input.continueFrom ? continueInput(root, input) : null;
|
|
381
|
+
const previousRun = continuation ? loadRun(root, continuation.runId) : null;
|
|
382
|
+
const workspaceRoot = previousRun?.workspaceRoot
|
|
383
|
+
? verifyWorkspaceRoot(previousRun.workspaceRoot)
|
|
384
|
+
: resolveWorkspaceRoot(previousRun?.workingDirectory ?? input.cwd);
|
|
226
385
|
const historyFile = selectHistory({ sessionDir: input.sessionDir, resume: input.resume });
|
|
386
|
+
if (input.command)
|
|
387
|
+
checkCommand(input.command, historyFile, input);
|
|
227
388
|
const account = this.options.accounts.resolve(input.accountId, input.poolId);
|
|
228
389
|
const context = this.options.accounts.context(account);
|
|
229
390
|
const runId = input.run.id;
|
|
@@ -239,7 +400,7 @@ export class Sessions {
|
|
|
239
400
|
const canAskUser = pinnedAskUser || (interactivePlan && harnessToolsEnabled);
|
|
240
401
|
const requestedTools = [...new Set([
|
|
241
402
|
...input.tools.allow.filter((name) => name !== SPAWN_SUBAGENT && name !== WAIT_SUBAGENTS && (name !== ASK_USER || canAskUser)),
|
|
242
|
-
...(harnessToolsEnabled ? [...(canAskUser ? [ASK_USER] : []), CHECKLIST_WRITE, SESSION_NAME, ...DEFAULT_TOOL_NAMES] : []),
|
|
403
|
+
...(harnessToolsEnabled ? [...(canAskUser ? [ASK_USER] : []), CHECKLIST_WRITE, CHECKPOINT_WRITE, SESSION_NAME, GET_CAPABILITIES, ...DEFAULT_TOOL_NAMES] : []),
|
|
243
404
|
...(broker?.toolNames ?? []),
|
|
244
405
|
...(canSpawn ? [SPAWN_SUBAGENT, WAIT_SUBAGENTS] : [])
|
|
245
406
|
])];
|
|
@@ -276,6 +437,7 @@ export class Sessions {
|
|
|
276
437
|
sessionId: input.run.sessionId,
|
|
277
438
|
nativeSessionId: null,
|
|
278
439
|
workingDirectory: input.cwd,
|
|
440
|
+
workspaceRoot,
|
|
279
441
|
worktreeId: input.run.worktreeId ?? null,
|
|
280
442
|
goal: input.run.goal ?? takeChars(input.text, 4000),
|
|
281
443
|
attachmentCount: input.run.attachmentCount ?? 0,
|
|
@@ -310,11 +472,11 @@ export class Sessions {
|
|
|
310
472
|
if (!existsSync(input.sessionDir))
|
|
311
473
|
mkdirSync(input.sessionDir, { recursive: true });
|
|
312
474
|
const launch = input.launch ?? {};
|
|
313
|
-
const plan = planSection(input.plan === true, historyFile);
|
|
475
|
+
const plan = planSection(input.plan === true, historyFile, input.run.relation === 'subagent');
|
|
314
476
|
const delivered = historyFile ? deliveredSnapshot(historyFile) : null;
|
|
315
477
|
const resumed = continuationSection(continuation, delivered?.some((entry) => entry.id === CONTINUATION_SECTION_ID) ?? false);
|
|
316
478
|
// The profiles replace the variant's own subagent list, which they include.
|
|
317
|
-
const profiles = capabilities.tools.includes(SPAWN_SUBAGENT) ? profileSection(root, input.cwd, variant?.dir ?? null) : null;
|
|
479
|
+
const profiles = capabilities.tools.includes(SPAWN_SUBAGENT) ? profileSection(root, input.cwd, variant?.dir ?? null, input.plan === true) : null;
|
|
318
480
|
if (profiles && variant)
|
|
319
481
|
variant = { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) };
|
|
320
482
|
const own = [plan, resumed, profiles].filter((section) => section !== null);
|
|
@@ -331,6 +493,9 @@ export class Sessions {
|
|
|
331
493
|
fail(error);
|
|
332
494
|
throw error;
|
|
333
495
|
}
|
|
496
|
+
// A reply is bounded only when its checkpoint tool is part of the frozen capability set.
|
|
497
|
+
const sliceEnabled = slicePolicy.enabled && capabilities.tools.includes(CHECKPOINT_WRITE);
|
|
498
|
+
const sliceState = createSliceState(slicePolicy, sliceOf(root, runId));
|
|
334
499
|
const run = this.options.runs.start(input.run.sessionId, runId);
|
|
335
500
|
if (run.finished)
|
|
336
501
|
return { runId, nativeSessionId: null };
|
|
@@ -353,6 +518,11 @@ export class Sessions {
|
|
|
353
518
|
});
|
|
354
519
|
try {
|
|
355
520
|
const tools = [
|
|
521
|
+
...(harnessToolsEnabled && allow.includes(GET_CAPABILITIES) ? [getCapabilitiesTool(() => ({
|
|
522
|
+
capabilities,
|
|
523
|
+
active: pi?.getActiveToolNames?.() ?? [],
|
|
524
|
+
registered: pi?.getAllTools?.() ?? []
|
|
525
|
+
}))] : []),
|
|
356
526
|
...BUILTIN_PLUGIN_REGISTRY.tools({
|
|
357
527
|
env: this.options.env,
|
|
358
528
|
caller,
|
|
@@ -379,12 +549,15 @@ export class Sessions {
|
|
|
379
549
|
}
|
|
380
550
|
}
|
|
381
551
|
const readOnly = new Set(tools.filter((tool) => tool.readOnly).map((tool) => tool.name));
|
|
552
|
+
const checkpointPlanInstructions = input.plan
|
|
553
|
+
? () => input.run.relation === 'subagent' ? PLAN_RESEARCH_SUBAGENT_INSTRUCTIONS : PLAN_INSTRUCTIONS
|
|
554
|
+
: undefined;
|
|
382
555
|
const planModeSafety = new Map();
|
|
383
556
|
for (const tool of tools)
|
|
384
557
|
if (tool.planModeSafe)
|
|
385
558
|
planModeSafety.set(tool.name, tool.planModeSafe);
|
|
386
559
|
const gate = (() => {
|
|
387
|
-
const baseGate = this.gate(run, caller, new Set((input.tools.gated ?? []).filter((name) => capabilities.tools.includes(name))));
|
|
560
|
+
const baseGate = this.gate(run, caller, new Set((input.tools.gated ?? []).filter((name) => capabilities.tools.includes(name))), false, input.automatic === true);
|
|
388
561
|
return {
|
|
389
562
|
...baseGate,
|
|
390
563
|
check: (name, args) => {
|
|
@@ -401,7 +574,9 @@ export class Sessions {
|
|
|
401
574
|
...(input.initialBashContext !== undefined && input.initialBashContext !== null ? { initialBashContext: input.initialBashContext } : {}),
|
|
402
575
|
gate,
|
|
403
576
|
restrictTools: input.run.relation === 'subagent',
|
|
577
|
+
...(input.plan ? { plan: true } : {}),
|
|
404
578
|
cwd: input.cwd,
|
|
579
|
+
workspaceRoot,
|
|
405
580
|
sessionDir: input.sessionDir,
|
|
406
581
|
historyFile,
|
|
407
582
|
model: input.model ?? null,
|
|
@@ -414,10 +589,20 @@ export class Sessions {
|
|
|
414
589
|
account: context,
|
|
415
590
|
resources: { ids: [], skillPaths: variant?.skillPaths ?? [], promptPaths: [] },
|
|
416
591
|
variantExtensions: variant?.extensionPaths ?? [],
|
|
417
|
-
inline: [
|
|
592
|
+
inline: [
|
|
593
|
+
...firstPartyFor(root, capabilities.contributions),
|
|
594
|
+
runLineageJoin(root, runId),
|
|
595
|
+
this.options.subagents.extension(runId),
|
|
596
|
+
{ name: 'image-pruning', factory: imagePruning() },
|
|
597
|
+
{ name: COMPACTION_CHECKPOINT_EXTENSION, factory: compactionCheckpoint({ initialChecklist: continuation?.checklist, planInstructions: checkpointPlanInstructions }) },
|
|
598
|
+
...(sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState }) }] : [])
|
|
599
|
+
],
|
|
418
600
|
...(plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }), loaded: (extensions) => plugins.loaded(extensions) } } : {}),
|
|
419
601
|
system,
|
|
420
|
-
|
|
602
|
+
...(sliceEnabled ? { slice: { state: sliceState, maxToolCalls: slicePolicy.maxToolCalls } } : {}),
|
|
603
|
+
// A command sends no request of its own, so the context update waits for the next turn.
|
|
604
|
+
update: input.command ? null : update,
|
|
605
|
+
...(input.command ? { command: input.command } : {})
|
|
421
606
|
};
|
|
422
607
|
pi = await this.factory({
|
|
423
608
|
params,
|
|
@@ -445,7 +630,7 @@ export class Sessions {
|
|
|
445
630
|
if (pi.model?.id && pi.model.provider)
|
|
446
631
|
this.options.subagents.resolvedModel(runId, `${pi.model.provider}/${pi.model.id}`, pi.thinkingLevel);
|
|
447
632
|
params.settle = () => this.options.subagents.verify(runId, opened.sessionManager ? subagentHistory(opened.sessionManager.getBranch()) : undefined);
|
|
448
|
-
void runTurn(run, pi, params);
|
|
633
|
+
void runTurn(run, pi, params).then(() => (sliceEnabled && !input.command ? this.continueSlice({ root, runId, source: input, policy: slicePolicy }) : undefined));
|
|
449
634
|
return { runId, nativeSessionId: pi.sessionId };
|
|
450
635
|
}
|
|
451
636
|
catch (error) {
|
|
@@ -15,6 +15,10 @@ export type SpawnedSubagent = Started & {
|
|
|
15
15
|
agent?: string;
|
|
16
16
|
unavailableTools?: string[];
|
|
17
17
|
};
|
|
18
|
+
export interface SubagentRuntimeSettings {
|
|
19
|
+
model: string | null;
|
|
20
|
+
effort: Effort | null;
|
|
21
|
+
}
|
|
18
22
|
export interface SubagentServiceOptions {
|
|
19
23
|
/** The plugin descriptors, so a profile's plugin list can drop the tools those plugins own. */
|
|
20
24
|
descriptors?: () => readonly PluginDescriptor[];
|
|
@@ -30,11 +34,16 @@ export declare class Subagents {
|
|
|
30
34
|
private readonly launch;
|
|
31
35
|
private readonly options;
|
|
32
36
|
private readonly parents;
|
|
37
|
+
private readonly runtimeSettingsByRun;
|
|
33
38
|
constructor(env: HarnessEnv, runs: RunRegistry, launch: (input: SessionRunInput) => Promise<Started>, options?: SubagentServiceOptions);
|
|
34
39
|
/** Internal adapter contract: context comes from a live run, never from model arguments. */
|
|
35
40
|
register(input: SessionRunInput, accountId: string, capabilities: CapabilitySet, deferUntilPrompt?: boolean): void;
|
|
41
|
+
runtimeSettings(runId: string): SubagentRuntimeSettings | null;
|
|
42
|
+
private rememberRuntimeSettings;
|
|
36
43
|
resolvedModel(runId: string, model: string, effort?: Effort): void;
|
|
37
44
|
private parent;
|
|
45
|
+
/** The plan-mode per-call gate and spawn implementation share this eligibility check. */
|
|
46
|
+
planSpawnRefusal(parentRunId: string, args: unknown): string | null;
|
|
38
47
|
spawn(request: SubagentInput): Promise<SpawnedSubagent>;
|
|
39
48
|
private selected;
|
|
40
49
|
results(selection: SubagentSelection): SubagentResult[];
|
|
@@ -6,6 +6,7 @@ import { now } from "../core/fs.js";
|
|
|
6
6
|
import { takeChars } from "../core/text.js";
|
|
7
7
|
import { isActiveStatus } from "./contracts.js";
|
|
8
8
|
import { narrowForProfile } from "../agents/narrow.js";
|
|
9
|
+
import { PLAN_RESEARCH_EVIDENCE_TOOLS, PLAN_RESEARCH_TOOLS, planResearchProfile } from "../agents/planResearch.js";
|
|
9
10
|
import { resolveProfile } from "../agents/profiles.js";
|
|
10
11
|
import { findRun, loadRun, readEvents, recordedSubagents, recordSubagent, saveRun } from "./runStore.js";
|
|
11
12
|
import { SPAWN_SUBAGENT, SUBAGENT_RELATION, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, briefTitle, subagentHistory, subagentTitle } from "./subagents.js";
|
|
@@ -27,6 +28,7 @@ export class Subagents {
|
|
|
27
28
|
launch;
|
|
28
29
|
options;
|
|
29
30
|
parents = new Map();
|
|
31
|
+
runtimeSettingsByRun = new Map();
|
|
30
32
|
constructor(env, runs, launch, options = {}) {
|
|
31
33
|
this.env = env;
|
|
32
34
|
this.runs = runs;
|
|
@@ -66,12 +68,26 @@ export class Subagents {
|
|
|
66
68
|
started();
|
|
67
69
|
this.parents.set(input.run.id, { ready, started, input: structuredClone({ ...input, text: '', images: undefined, requiresImageInput: false, preamble: null, resume: null }), accountId, capabilities: ceiling, stopped: new AbortController(), recovered: new Set(), launching: new Map() });
|
|
68
70
|
}
|
|
71
|
+
runtimeSettings(runId) {
|
|
72
|
+
return this.runtimeSettingsByRun.get(runId) ?? null;
|
|
73
|
+
}
|
|
74
|
+
rememberRuntimeSettings(runId, settings) {
|
|
75
|
+
this.runtimeSettingsByRun.set(runId, settings);
|
|
76
|
+
if (this.runtimeSettingsByRun.size > 500) {
|
|
77
|
+
const oldest = this.runtimeSettingsByRun.keys().next().value;
|
|
78
|
+
if (oldest !== undefined)
|
|
79
|
+
this.runtimeSettingsByRun.delete(oldest);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
69
82
|
resolvedModel(runId, model, effort) {
|
|
70
83
|
const parent = this.parents.get(runId);
|
|
71
84
|
if (parent) {
|
|
72
85
|
parent.input.model = model;
|
|
73
86
|
parent.input.effort = effort ?? parent.input.effort;
|
|
74
87
|
}
|
|
88
|
+
const settings = this.runtimeSettingsByRun.get(runId);
|
|
89
|
+
if (settings)
|
|
90
|
+
this.rememberRuntimeSettings(runId, { model, effort: effort ?? settings.effort });
|
|
75
91
|
}
|
|
76
92
|
parent(runId) {
|
|
77
93
|
const parent = this.parents.get(runId);
|
|
@@ -79,6 +95,34 @@ export class Subagents {
|
|
|
79
95
|
throw new HarnessError('run_not_active', 'The parent run is not accepting subagents.');
|
|
80
96
|
return parent;
|
|
81
97
|
}
|
|
98
|
+
/** The plan-mode per-call gate and spawn implementation share this eligibility check. */
|
|
99
|
+
planSpawnRefusal(parentRunId, args) {
|
|
100
|
+
const parent = this.parents.get(parentRunId);
|
|
101
|
+
if (!parent || !this.runs.active(parentRunId))
|
|
102
|
+
return 'The parent run is not accepting subagents.';
|
|
103
|
+
if (!parent.input.plan)
|
|
104
|
+
return 'Read-only research delegation is available only during plan mode.';
|
|
105
|
+
if (!parent.capabilities.tools.includes(SPAWN_SUBAGENT))
|
|
106
|
+
return 'Subagents are not enabled for this run.';
|
|
107
|
+
if (!args || typeof args !== 'object' || Array.isArray(args))
|
|
108
|
+
return 'Plan-mode delegation requires a named, eligible read-only research profile.';
|
|
109
|
+
const request = args;
|
|
110
|
+
if (typeof request.agent !== 'string' || !request.agent.trim())
|
|
111
|
+
return 'Plan-mode delegation requires a named, eligible read-only research profile.';
|
|
112
|
+
const profile = resolveProfile(this.env.root, parent.input.cwd, request.agent.trim(), parent.input.variant && 'dir' in parent.input.variant ? parent.input.variant.dir : null);
|
|
113
|
+
if (!profile)
|
|
114
|
+
return `There is no agent profile named ${request.agent.trim()}.`;
|
|
115
|
+
if (!planResearchProfile(profile))
|
|
116
|
+
return 'This profile is not eligible for plan-mode research; use an explicit read-only research profile.';
|
|
117
|
+
if (request.tools !== undefined && (!Array.isArray(request.tools) || request.tools.some(name => typeof name !== 'string' || !PLAN_RESEARCH_TOOLS.has(name) || !profile.tools.includes(name) || !parent.capabilities.tools.includes(name)))) {
|
|
118
|
+
return 'Plan-mode subagent tools must be plan-safe and included in the selected profile and parent capabilities.';
|
|
119
|
+
}
|
|
120
|
+
const requested = Array.isArray(request.tools) ? request.tools : null;
|
|
121
|
+
const available = profile.tools.filter(name => parent.capabilities.tools.includes(name) && (!requested || requested.includes(name)));
|
|
122
|
+
if (!available.some(name => PLAN_RESEARCH_EVIDENCE_TOOLS.has(name)))
|
|
123
|
+
return 'The selected profile has no available read-only research tools in this run.';
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
82
126
|
async spawn(request) {
|
|
83
127
|
const parent = this.parent(request.parentRunId);
|
|
84
128
|
const { input, accountId, capabilities } = parent;
|
|
@@ -97,6 +141,11 @@ export class Subagents {
|
|
|
97
141
|
const outside = request.tools?.filter((name) => !capabilities.tools.includes(name)) ?? [];
|
|
98
142
|
if (outside.length)
|
|
99
143
|
throw invalid(`A subagent cannot have tools you do not have: ${outside.join(', ')}.`);
|
|
144
|
+
if (input.plan) {
|
|
145
|
+
const refusal = this.planSpawnRefusal(request.parentRunId, request);
|
|
146
|
+
if (refusal)
|
|
147
|
+
throw invalid(refusal);
|
|
148
|
+
}
|
|
100
149
|
await until(parent.ready, parent.stopped.signal);
|
|
101
150
|
let variant = input.variant ?? null;
|
|
102
151
|
let launch = input.launch;
|
|
@@ -122,20 +171,30 @@ export class Subagents {
|
|
|
122
171
|
effort = profile.effort;
|
|
123
172
|
launch = { ...(input.launch ?? {}), sections: [...(input.launch?.sections ?? []).filter((section) => section.id !== 'subagent'), { id: 'subagent', title: `Your role: ${profile.name}`, body: profile.instructions || profile.description, budget: 16_000, order: 22 }] };
|
|
124
173
|
}
|
|
174
|
+
if (input.plan) {
|
|
175
|
+
const refusal = this.planSpawnRefusal(request.parentRunId, request);
|
|
176
|
+
if (refusal)
|
|
177
|
+
throw invalid(refusal);
|
|
178
|
+
}
|
|
125
179
|
const narrowed = narrowForProfile(capabilities, profile, request.tools, this.options.descriptors?.() ?? []);
|
|
126
|
-
const
|
|
180
|
+
const childCapabilities = input.plan
|
|
181
|
+
? { ...narrowed.capabilities, tools: narrowed.capabilities.tools.filter(name => PLAN_RESEARCH_TOOLS.has(name)), connectors: [] }
|
|
182
|
+
: narrowed.capabilities;
|
|
183
|
+
const tools = childCapabilities.tools;
|
|
127
184
|
const runId = randomUUID();
|
|
128
185
|
const sessionId = randomUUID();
|
|
186
|
+
this.rememberRuntimeSettings(runId, { model: model ?? null, effort: effort ?? null });
|
|
129
187
|
recordSubagent(this.env.root, request.parentRunId, runId);
|
|
130
188
|
const child = {
|
|
131
189
|
...input,
|
|
132
|
-
run: { ...input.run, id: runId, sessionId, parentRunId: request.parentRunId, relation: SUBAGENT_RELATION, goal: takeChars(brief, 4000), title, attachmentCount: 0, resumedFromRunId: null, handoffId: null, profile: profile?.name ?? null },
|
|
190
|
+
run: { ...input.run, ...(input.plan ? { worktreeId: null } : {}), id: runId, sessionId, parentRunId: request.parentRunId, relation: SUBAGENT_RELATION, goal: takeChars(brief, 4000), title, attachmentCount: 0, resumedFromRunId: null, handoffId: null, profile: profile?.name ?? null },
|
|
133
191
|
accountId, poolId: null, sessionDir: join(input.sessionDir, 'subagents', runId), resume: null,
|
|
134
192
|
text: brief, images: undefined, requiresImageInput: false, preamble: null,
|
|
135
193
|
caller: { ...input.caller, runId: undefined, sessionId },
|
|
136
194
|
tools: { allow: tools, gated: (input.tools.gated ?? []).filter((name) => tools.includes(name)) },
|
|
137
|
-
capabilities:
|
|
138
|
-
variant, launch, model, effort
|
|
195
|
+
capabilities: childCapabilities, depth: (input.depth ?? 0) + 1,
|
|
196
|
+
variant, launch, model, effort,
|
|
197
|
+
...(input.plan ? { connections: false, subagents: null } : {})
|
|
139
198
|
};
|
|
140
199
|
const started = this.launch(child).catch((error) => {
|
|
141
200
|
if (!findRun(this.env.root, runId)) {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Checkpoint } from "./contracts.js";
|
|
2
|
+
export declare const WORK_SLICE_EXTENSION = "work-slice";
|
|
3
|
+
export interface WorkSliceInput {
|
|
4
|
+
/** False turns the guard off for this run: no budget, no stall notes, no continuation. */
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
/** Tool calls in one reply; 0 disables this limit. */
|
|
7
|
+
maxToolCalls?: number;
|
|
8
|
+
/** Minutes in one reply; 0 disables this limit. */
|
|
9
|
+
maxMinutes?: number;
|
|
10
|
+
/** New tokens in one reply (input + cache read + output); 0 disables this limit. */
|
|
11
|
+
maxNewTokens?: number;
|
|
12
|
+
/** Slices one task may use before it is handed back to the person; 0 disables the cap. */
|
|
13
|
+
maxSlices?: number;
|
|
14
|
+
/** Start the next slice from the checkpoint without asking. */
|
|
15
|
+
continue?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface SlicePolicy {
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
maxToolCalls: number;
|
|
20
|
+
maxMinutes: number;
|
|
21
|
+
maxNewTokens: number;
|
|
22
|
+
maxSlices: number;
|
|
23
|
+
continue: boolean;
|
|
24
|
+
}
|
|
25
|
+
/** The foundation defaults: enough for one meaningful step of work, short enough to report. */
|
|
26
|
+
export declare const DEFAULT_SLICE_POLICY: SlicePolicy;
|
|
27
|
+
export declare class WorkSliceError extends Error {
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The policy a run asked for, or the defaults. `XINGULAR_WORK_SLICE=off` at launch turns the guard
|
|
31
|
+
* off for every run in this process; nothing is persisted, so restarting without it restores the
|
|
32
|
+
* defaults.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseWorkSlice(input: unknown): SlicePolicy;
|
|
35
|
+
export type SliceEnd = 'budget' | 'stuck' | 'compaction-thrash';
|
|
36
|
+
/** What a live reply tracks for its slice; the runner reads it after the reply. */
|
|
37
|
+
export interface SliceState {
|
|
38
|
+
slice: number;
|
|
39
|
+
steps: number;
|
|
40
|
+
tokens: number;
|
|
41
|
+
startedAt: number;
|
|
42
|
+
lastTool: string | null;
|
|
43
|
+
compactions: number;
|
|
44
|
+
checkpointWritten: boolean;
|
|
45
|
+
/** Set when the guard ended the reply instead of the model finishing it. */
|
|
46
|
+
endedBy: SliceEnd | null;
|
|
47
|
+
/** The limit that ended it, for the record. */
|
|
48
|
+
endedByLimit: string | null;
|
|
49
|
+
/** Steps past which every non-exempt tool call is refused, once one was refused. */
|
|
50
|
+
refusedAt: number | null;
|
|
51
|
+
}
|
|
52
|
+
export declare function createSliceState(_policy: SlicePolicy, slice?: number, now?: number): SliceState;
|
|
53
|
+
/** Counters start again for each reply; a terminal session keeps one guard across replies. */
|
|
54
|
+
export declare function resetSliceState(state: SliceState, now?: number): void;
|
|
55
|
+
/** The first limit this reply has passed, named as a person would say it. */
|
|
56
|
+
export declare function sliceLimitReached(policy: SlicePolicy, state: SliceState, now?: number): string | null;
|
|
57
|
+
/** Commands that read as a check, for "no progress" and "still failing" rules. */
|
|
58
|
+
export declare function looksLikeCheck(command: string): boolean;
|
|
59
|
+
interface ToolCall {
|
|
60
|
+
toolName: string;
|
|
61
|
+
input?: unknown;
|
|
62
|
+
}
|
|
63
|
+
interface ToolResult {
|
|
64
|
+
toolName: string;
|
|
65
|
+
isError?: boolean;
|
|
66
|
+
input?: unknown;
|
|
67
|
+
}
|
|
68
|
+
/** The part of Pi's extension API this guard uses. Structural, so the module needs no Pi import. */
|
|
69
|
+
interface GuardApi {
|
|
70
|
+
on(event: 'agent_start', handler: () => unknown): void;
|
|
71
|
+
on(event: 'tool_call', handler: (event: ToolCall) => unknown): void;
|
|
72
|
+
on(event: 'tool_result', handler: (event: ToolResult) => unknown): void;
|
|
73
|
+
on(event: 'message_end', handler: (event: {
|
|
74
|
+
message: {
|
|
75
|
+
role: string;
|
|
76
|
+
usage?: {
|
|
77
|
+
input?: number;
|
|
78
|
+
output?: number;
|
|
79
|
+
cacheRead?: number;
|
|
80
|
+
cacheWrite?: number;
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
}) => unknown): void;
|
|
84
|
+
}
|
|
85
|
+
export interface WorkSliceGuardOptions {
|
|
86
|
+
policy: SlicePolicy;
|
|
87
|
+
state: SliceState;
|
|
88
|
+
/** Called once the guard has decided the reply must stop; the runner ends it. */
|
|
89
|
+
onEnd?: (end: SliceEnd, limit: string) => void;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Bounds one reply and names repeated work. The first refusal asks for a checkpoint; a tool call
|
|
93
|
+
* after that refusal ends the reply. A repeated call or failing command is named once, then ends it.
|
|
94
|
+
*/
|
|
95
|
+
export declare function workSliceGuard(options: WorkSliceGuardOptions): (pi: GuardApi) => void;
|
|
96
|
+
/**
|
|
97
|
+
* Which slice of its session a run is, from 1. Counts the recorded continuation chain, so it
|
|
98
|
+
* survives a restart and needs no extra field on the run record.
|
|
99
|
+
*/
|
|
100
|
+
export declare function sliceOf(root: string, runId: string, limit?: number): number;
|
|
101
|
+
/** The reason a checkpoint the harness derives (no model handoff) records. */
|
|
102
|
+
export declare function derivedReason(end: SliceEnd | null): Checkpoint['reason'];
|
|
103
|
+
export {};
|