@xingular/harness 1.4.0-unstable → 1.5.0-unstable
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 +41 -1
- package/NOTICE.md +9 -7
- package/dist/cli.js +160 -456
- package/dist/sdk/prompt/compile.d.ts +4 -2
- package/dist/sdk/prompt/compile.js +10 -8
- package/dist/sdk/prompt/reporting.d.ts +2 -2
- package/dist/sdk/prompt/reporting.js +40 -250
- package/dist/sdk/prompt/standard.d.ts +4 -1
- package/dist/sdk/prompt/standard.js +6 -2
- package/dist/sdk/sessions/completionCheck.d.ts +4 -2
- package/dist/sdk/sessions/completionCheck.js +19 -1
- package/dist/sdk/sessions/contracts.js +2 -2
- package/dist/sdk/sessions/goalStop.d.ts +27 -0
- package/dist/sdk/sessions/goalStop.js +41 -0
- package/dist/sdk/sessions/runner.d.ts +0 -3
- package/dist/sdk/sessions/runner.js +16 -9
- package/dist/sdk/sessions/sessions.js +6 -36
- package/dist/sdk/sessions/workSlice.js +1 -1
- package/dist/sdk/terminal/piExtension.js +18 -31
- package/dist/sdk/terminal/terminals.js +1 -1
- package/dist/sdk/tools/askUser.js +1 -1
- package/dist/sdk/tools/todo.js +4 -3
- package/dist/terminal-opentui.js +108 -200
- package/dist/terminal-pi.js +262 -338
- package/docs/architecture/02-sessions-and-runs.md +13 -15
- package/docs/architecture/04-prompt.md +41 -35
- package/docs/architecture/07-tools-and-gate.md +2 -2
- package/docs/architecture/10-server-protocol.md +6 -5
- package/docs/architecture/11-terminal.md +7 -7
- package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +2 -2
- package/docs/decisions/ADR-0069-stop-gate-and-finish.md +8 -2
- package/docs/decisions/ADR-0070-terminal-slice-continuation.md +4 -1
- package/docs/decisions/ADR-0072-codex-prompts-and-goal.md +3 -0
- package/docs/decisions/ADR-0073-large-reporting-prompt.md +6 -1
- package/docs/decisions/ADR-0074-report-content-not-report-blocks.md +90 -0
- package/docs/decisions/ADR-0075-no-todo-gate.md +84 -0
- package/docs/decisions/ADR-0076-codex-final-answer-verbatim.md +93 -0
- package/docs/releases/1.4.0-unstable.md +9 -2
- package/docs/releases/1.5.0-unstable.md +23 -0
- package/docs/study/STUDY-0004-turn-end-controls.md +7 -0
- package/docs/study/STUDY-0006-reporting-prompts.md +48 -10
- package/package.json +1 -1
- package/dist/sdk/sessions/stopGate.d.ts +0 -78
- package/dist/sdk/sessions/stopGate.js +0 -143
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The goal part of a stop: a /goal that ended blocked, paused or at a limit leaves work for the
|
|
2
|
+
// person, and its checkpoint's done/next comes from the reply's todo list. An ordinary reply has no
|
|
3
|
+
// gate and settles as it ends (ADR-0075).
|
|
4
|
+
import { MAX_CHECKPOINT_DONE } from "./contracts.js";
|
|
5
|
+
/** The todo items a reply has not finished. */
|
|
6
|
+
export function openItems(todo) {
|
|
7
|
+
return todo.filter((item) => item.status !== 'completed');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* `goal` is the session's /goal, when it has one: a goal that ended blocked needs the person, and
|
|
11
|
+
* one that ended on a limit or a pause leaves work (ADR-0072).
|
|
12
|
+
*/
|
|
13
|
+
export function goalStopOutcome(todo, goal) {
|
|
14
|
+
const open = openItems(todo);
|
|
15
|
+
if (goal?.status === 'blocked')
|
|
16
|
+
return { code: 'needs_person', reason: goal.lastReason || 'The goal is blocked.', open };
|
|
17
|
+
if (goal && (goal.status === 'budget_limited' || goal.status === 'usage_limited' || goal.status === 'paused')) {
|
|
18
|
+
return { code: 'unfinished', reason: goal.lastReason || `The goal is ${goal.status.replace('_', ' ')}.`, open };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
/** What a checkpoint learns from the reply's todo list and the goal. */
|
|
23
|
+
export function handoffFields(todo, goal) {
|
|
24
|
+
const next = openItems(todo)[0]?.text || '';
|
|
25
|
+
const blocked = goal?.status === 'blocked' ? (goal.lastReason || 'The goal is blocked.').slice(0, 500) : '';
|
|
26
|
+
return {
|
|
27
|
+
done: todo.filter((item) => item.status === 'completed').slice(0, MAX_CHECKPOINT_DONE).map((item) => ({ item: item.text.slice(0, 300), evidence: [] })),
|
|
28
|
+
next: next.slice(0, 500),
|
|
29
|
+
...(blocked ? { blocked } : {})
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Why a checkpoint written for a stop outcome was written. */
|
|
33
|
+
export const stopCheckpointReason = (outcome) => outcome.code === 'needs_person' ? 'requested' : 'stuck';
|
|
34
|
+
/** The summary a stop outcome's checkpoint falls back to when the reply left no text. */
|
|
35
|
+
export function stopSummary(outcome) {
|
|
36
|
+
if (outcome.code === 'needs_person')
|
|
37
|
+
return `The reply stopped for the person: ${outcome.reason}`.slice(0, 1_200);
|
|
38
|
+
if (outcome.reason)
|
|
39
|
+
return `The reply ended with work left: ${outcome.reason}`.slice(0, 1_200);
|
|
40
|
+
return `The reply ended with ${outcome.open.length} open todo item${outcome.open.length === 1 ? '' : 's'}.`;
|
|
41
|
+
}
|
|
@@ -8,7 +8,6 @@ import { type ContextUpdate } from "../prompt/compile.js";
|
|
|
8
8
|
import type { ToolSpec } from "../tools/types.js";
|
|
9
9
|
import { type SliceState } from "./workSlice.js";
|
|
10
10
|
import type { GoalController } from "./goal.js";
|
|
11
|
-
import { type StopGateState } from "./stopGate.js";
|
|
12
11
|
import { type ExtensionHost } from "./extensionUi.js";
|
|
13
12
|
import type { ActiveRun } from "./registry.js";
|
|
14
13
|
export { EFFORTS };
|
|
@@ -86,8 +85,6 @@ export interface TurnParams {
|
|
|
86
85
|
state: SliceState;
|
|
87
86
|
maxToolCalls: number;
|
|
88
87
|
};
|
|
89
|
-
/** The stop gate's view of this reply (ADR-0069): its todo list. */
|
|
90
|
-
stopGate?: StopGateState;
|
|
91
88
|
/** The run's /goal (ADR-0072); the runner counts a claim check's model request with the run's usage. */
|
|
92
89
|
goal?: {
|
|
93
90
|
controller: GoalController;
|
|
@@ -10,7 +10,9 @@ import { CONTEXT_MESSAGE_TYPE, CONTEXT_SNAPSHOT_KEY } from "../prompt/compile.js
|
|
|
10
10
|
import { PLAN_REPAIR_CUSTOM_TYPE, PLAN_REPAIR_INSTRUCTIONS, finalizePlanOutcome } from "./planMode.js";
|
|
11
11
|
import { MAX_CHECKPOINT_CHECKS, MAX_CHECKPOINT_FILES, MAX_CHECKPOINT_SUMMARY } from "./contracts.js";
|
|
12
12
|
import { derivedReason } from "./workSlice.js";
|
|
13
|
-
import {
|
|
13
|
+
import { goalStopOutcome, handoffFields, stopCheckpointReason, stopSummary } from "./goalStop.js";
|
|
14
|
+
import { TODO_WRITE } from "../tools/todo.js";
|
|
15
|
+
import { validatedTodo } from "./compactionCheckpoint.js";
|
|
14
16
|
import { toolAttachmentRefsSchema } from "./contracts.js";
|
|
15
17
|
import { completedReasoningSummary, REASONING_SUMMARY_APIS, requestDetailedReasoningSummary } from "./reasoningSummaries.js";
|
|
16
18
|
import { extensionUi } from "./extensionUi.js";
|
|
@@ -163,10 +165,10 @@ const lastLine = (text) => {
|
|
|
163
165
|
* Files and checks come from the reply's tool calls, done and next from its todo list, and blocked
|
|
164
166
|
* from a goal that ended blocked (ADR-0069, ADR-0072).
|
|
165
167
|
*/
|
|
166
|
-
function derivedCheckpoint(ledger, slice, reason, fallback,
|
|
168
|
+
function derivedCheckpoint(ledger, slice, reason, fallback, goal) {
|
|
167
169
|
return {
|
|
168
170
|
summary: ledger.lastText.trim().slice(0, MAX_CHECKPOINT_SUMMARY) || fallback,
|
|
169
|
-
...handoffFields(
|
|
171
|
+
...handoffFields(ledger.todo, goal),
|
|
170
172
|
files: [...ledger.files].slice(-MAX_CHECKPOINT_FILES).map(([path, why]) => ({ path: path.slice(0, 300), why: why.slice(0, 200) })),
|
|
171
173
|
checks: ledger.checks.slice(-MAX_CHECKPOINT_CHECKS),
|
|
172
174
|
decisions: [],
|
|
@@ -191,7 +193,7 @@ export async function runTurn(run, session, params) {
|
|
|
191
193
|
let costReported = false;
|
|
192
194
|
const toolStartedAt = new Map();
|
|
193
195
|
const slice = params.slice?.state ?? null;
|
|
194
|
-
const ledger = { files: new Map(), checks: [], lastText: '' };
|
|
196
|
+
const ledger = { files: new Map(), checks: [], lastText: '', todo: [] };
|
|
195
197
|
const bashCommands = new Map();
|
|
196
198
|
const heartbeat = slice
|
|
197
199
|
? setInterval(() => {
|
|
@@ -416,6 +418,11 @@ export async function runTurn(run, session, params) {
|
|
|
416
418
|
if (typeof command === 'string')
|
|
417
419
|
bashCommands.set(tool.toolCallId, command);
|
|
418
420
|
}
|
|
421
|
+
if (tool.toolName === TODO_WRITE) {
|
|
422
|
+
const items = validatedTodo(tool.args && typeof tool.args === 'object' ? tool.args.items : undefined);
|
|
423
|
+
if (items)
|
|
424
|
+
ledger.todo = items;
|
|
425
|
+
}
|
|
419
426
|
break;
|
|
420
427
|
}
|
|
421
428
|
case 'tool_execution_end': {
|
|
@@ -619,16 +626,16 @@ export async function runTurn(run, session, params) {
|
|
|
619
626
|
}
|
|
620
627
|
if (slice?.endedBy && !aborted && !lastError) {
|
|
621
628
|
// The model stopped because its slice was over. The harness keeps the state for the next slice.
|
|
622
|
-
run.emit('checkpoint', derivedCheckpoint(ledger, slice.slice, derivedReason(slice.endedBy), `The reply stopped after ${slice.steps} steps at its work-slice boundary.`, params.
|
|
629
|
+
run.emit('checkpoint', derivedCheckpoint(ledger, slice.slice, derivedReason(slice.endedBy), `The reply stopped after ${slice.steps} steps at its work-slice boundary.`, params.goal?.controller.get()));
|
|
623
630
|
run.finish('interrupted', undefined, 'slice_complete', slice.endedBy === 'budget' ? 'slice_budget' : 'compaction_thrash');
|
|
624
631
|
return;
|
|
625
632
|
}
|
|
626
|
-
//
|
|
627
|
-
//
|
|
633
|
+
// An ordinary reply settles as it ends (ADR-0075). Only a goal that was blocked, paused or ran
|
|
634
|
+
// into a limit stops it for the person, and then the checkpoint carries what is left.
|
|
628
635
|
const goalState = params.goal?.controller.get() ?? null;
|
|
629
|
-
const stop =
|
|
636
|
+
const stop = goalState && !aborted && !lastError ? goalStopOutcome(ledger.todo, goalState) : null;
|
|
630
637
|
if (stop) {
|
|
631
|
-
run.emit('checkpoint', derivedCheckpoint(ledger, slice?.slice ?? 1, stopCheckpointReason(stop), stopSummary(stop),
|
|
638
|
+
run.emit('checkpoint', derivedCheckpoint(ledger, slice?.slice ?? 1, stopCheckpointReason(stop), stopSummary(stop), goalState));
|
|
632
639
|
run.finish('interrupted', undefined, stop.code, stop.code);
|
|
633
640
|
return;
|
|
634
641
|
}
|
|
@@ -22,8 +22,7 @@ import { ASK_USER } from "../tools/askUser.js";
|
|
|
22
22
|
import { defaultSkillPaths } from "../tools/defaultSkills.js";
|
|
23
23
|
import { TODO_WRITE } from "../tools/todo.js";
|
|
24
24
|
import { checkpointOf } from "./checkpoint.js";
|
|
25
|
-
import {
|
|
26
|
-
import { launchSwitchOff } from "./completionCheck.js";
|
|
25
|
+
import { launchSwitchOff, sessionCheckModel } from "./completionCheck.js";
|
|
27
26
|
import { GOAL_TOOLS, GoalController, GoalError, continuationPrompt, goalTools, provenanceText } from "./goal.js";
|
|
28
27
|
import { GOAL_EXTENSION, goalLoop, goalNotice } from "./goalLoop.js";
|
|
29
28
|
import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
|
|
@@ -64,24 +63,6 @@ function checkCommand(command, historyFile, input) {
|
|
|
64
63
|
}
|
|
65
64
|
}
|
|
66
65
|
const CONTINUE_TEXT = 'Continue from where you stopped.';
|
|
67
|
-
/** The person's request behind a chain of continuations: the first goal that is not the continue text. */
|
|
68
|
-
function requestOfChain(root, runId) {
|
|
69
|
-
const seen = new Set();
|
|
70
|
-
for (let current = runId; current && !seen.has(current);) {
|
|
71
|
-
seen.add(current);
|
|
72
|
-
let record;
|
|
73
|
-
try {
|
|
74
|
-
record = loadRun(root, current);
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
if (record.goal && record.goal.trim() !== CONTINUE_TEXT)
|
|
80
|
-
return record.goal;
|
|
81
|
-
current = record.resumedFromRunId ?? null;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
66
|
/** Fills a continuing run's input from the run it continues, refusing anything that would differ. */
|
|
86
67
|
function continueInput(root, input) {
|
|
87
68
|
const continuation = continuationOf(root, input.continueFrom, input.sessionDir);
|
|
@@ -557,6 +538,7 @@ export class Sessions {
|
|
|
557
538
|
const own = [plan, resumed, profiles].filter((section) => section !== null);
|
|
558
539
|
const effectiveLaunch = {
|
|
559
540
|
...launch,
|
|
541
|
+
model: input.model ?? launch.model ?? null,
|
|
560
542
|
...(sessionState ? { sessionState } : {}),
|
|
561
543
|
sections: own.length ? [...(launch.sections ?? []).filter((section) => !own.some((entry) => entry.id === section.id)), ...own] : launch.sections,
|
|
562
544
|
tools: capabilities.tools,
|
|
@@ -575,12 +557,9 @@ export class Sessions {
|
|
|
575
557
|
const planResearchChild = input.plan === true && input.run.relation === 'subagent';
|
|
576
558
|
const sliceEnabled = slicePolicy.enabled && (harnessStateTools || planResearchChild);
|
|
577
559
|
const sliceState = createSliceState(slicePolicy, sliceOf(root, runId));
|
|
578
|
-
// The stop gate (ADR-
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
// The completion check judges the person's request, not "Continue from where you stopped.".
|
|
582
|
-
const originalRequest = input.text.trim() === CONTINUE_TEXT ? requestOfChain(root, previousRun?.id ?? null) : null;
|
|
583
|
-
const stopGateState = gateEnabled ? createStopGateState(continuation?.todo) : null;
|
|
560
|
+
// The stop gate stays gone (ADR-0075): a reply the model ends settles as it ends, and the todo
|
|
561
|
+
// list is progress, not a gate. The goal loop below is the only layer that continues a reply.
|
|
562
|
+
// The completion check judges the goal's claim, not "Continue from where you stopped."
|
|
584
563
|
// The goal: a continuation slice picks up the previous run's goal where it stopped.
|
|
585
564
|
let goalController = null;
|
|
586
565
|
if (goalWanted && input.goal) {
|
|
@@ -706,15 +685,7 @@ export class Sessions {
|
|
|
706
685
|
{ name: COMPACTION_CHECKPOINT_EXTENSION, factory: compactionCheckpoint({ initialTodo: continuation?.todo, planInstructions: checkpointPlanInstructions }) },
|
|
707
686
|
...(sliceEnabled ? [{ name: WORK_SLICE_EXTENSION, factory: workSliceGuard({ policy: slicePolicy, state: sliceState, onRefuse: (event, reason) => { if (typeof event.toolCallId === 'string')
|
|
708
687
|
run.emit('tool.refused', { toolCallId: event.toolCallId, name: event.toolName, code: 'work_slice', reason: reason.slice(0, 2000) }); } }) }] : []),
|
|
709
|
-
//
|
|
710
|
-
...(stopGateState ? [{ name: STOP_GATE_EXTENSION, factory: stopGate({
|
|
711
|
-
state: stopGateState,
|
|
712
|
-
initialTodo: continuation?.todo,
|
|
713
|
-
request: () => originalRequest,
|
|
714
|
-
sliceEnded: () => sliceEnabled && sliceState.endedBy !== null,
|
|
715
|
-
onEvent: (event) => run.emit('extension.notice', { kind: 'notify', level: 'info', extension: STOP_GATE_EXTENSION, text: stopGateNotice(event) })
|
|
716
|
-
}) }] : []),
|
|
717
|
-
// After the todo gate: a goal continues a reply the gate let end (ADR-0072).
|
|
688
|
+
// A goal continues a reply the model ended (ADR-0072).
|
|
718
689
|
...(goalController ? [{ name: GOAL_EXTENSION, factory: goalLoop({
|
|
719
690
|
controller: goalController,
|
|
720
691
|
check: launchSwitchOff('XINGULAR_COMPLETION_CHECK') ? null : sessionCheckModel,
|
|
@@ -727,7 +698,6 @@ export class Sessions {
|
|
|
727
698
|
activatedPlugins: (ids) => updateRun(root, runId, (record) => { record.activatedPlugins = ids; }),
|
|
728
699
|
system,
|
|
729
700
|
...(sliceEnabled ? { slice: { state: sliceState, maxToolCalls: slicePolicy.maxToolCalls } } : {}),
|
|
730
|
-
...(stopGateState ? { stopGate: stopGateState } : {}),
|
|
731
701
|
...(goalController ? { goal: goalHandle } : {}),
|
|
732
702
|
// A command sends no request of its own, so the context update waits for the next turn.
|
|
733
703
|
update: input.command ? null : update,
|
|
@@ -143,7 +143,7 @@ export function workSliceGuard(options) {
|
|
|
143
143
|
return { block: true, terminate: true, reason };
|
|
144
144
|
};
|
|
145
145
|
return (pi) => {
|
|
146
|
-
// Pi emits agent_start again for every continuation (a
|
|
146
|
+
// Pi emits agent_start again for every continuation (a slice continuation, delivered subagent
|
|
147
147
|
// results, a plan repair), and those belong to the same reply and the same budget. Only a
|
|
148
148
|
// person's message starts a new slice.
|
|
149
149
|
pi.on('before_agent_start', (event) => {
|
|
@@ -12,11 +12,11 @@ import { SPAWN_SUBAGENT, SUBAGENT_OWNER, SUBAGENT_RECEIPT, SUBAGENT_RESULTS, sub
|
|
|
12
12
|
import { PLAN_RESEARCH_TOOLS } from "../agents/planResearch.js";
|
|
13
13
|
import { askUserTool } from "../tools/askUser.js";
|
|
14
14
|
import { TODO_WRITE, todoTool } from "../tools/todo.js";
|
|
15
|
-
import {
|
|
16
|
-
import { launchSwitchOff } from "../sessions/completionCheck.js";
|
|
15
|
+
import { goalStopOutcome, handoffFields, stopCheckpointReason, stopSummary } from "../sessions/goalStop.js";
|
|
16
|
+
import { launchSwitchOff, sessionCheckModel } from "../sessions/completionCheck.js";
|
|
17
17
|
import { GOAL_STATE_ENTRY, GOAL_TOOLS, goalTools } from "../sessions/goal.js";
|
|
18
18
|
import { GOAL_EXTENSION, goalLoop, goalNotice } from "../sessions/goalLoop.js";
|
|
19
|
-
import { createSliceState, DEFAULT_SLICE_POLICY, parseWorkSlice, sliceContinuation, workSliceGuard } from "../sessions/workSlice.js";
|
|
19
|
+
import { createSliceState, DEFAULT_SLICE_POLICY, parseWorkSlice, sliceContinuation, workSliceGuard, WORK_SLICE_EXTENSION } from "../sessions/workSlice.js";
|
|
20
20
|
import { DEFAULT_COMMAND_TIMEOUT_SECONDS, withCommandBudget } from "../core/commandBudget.js";
|
|
21
21
|
import { SESSION_NAME, deriveSessionName, sessionNameTool } from "../tools/sessionName.js";
|
|
22
22
|
import { CAPABILITY_DISCOVERY, describeCapabilities } from "../tools/getCapabilities.js";
|
|
@@ -190,8 +190,6 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
190
190
|
}
|
|
191
191
|
const sliceState = createSliceState(slicePolicy);
|
|
192
192
|
// `XINGULAR_WORK_SLICE=off` at launch leaves terminal replies unbounded, as before PROP-0041.
|
|
193
|
-
// One stop-gate state per terminal session; each reply resets it.
|
|
194
|
-
const gateState = createStopGateState();
|
|
195
193
|
// A goal-claim check's model request counts with the reply's usage, like any other request.
|
|
196
194
|
const recordCheckUsage = (raw) => {
|
|
197
195
|
const message = raw;
|
|
@@ -490,21 +488,10 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
490
488
|
continue: true
|
|
491
489
|
};
|
|
492
490
|
});
|
|
493
|
-
// The stop gate (ADR-
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
stopGate({
|
|
498
|
-
state: gateState,
|
|
499
|
-
active: () => !planRun?.active,
|
|
500
|
-
sliceEnded: () => sliceState.endedBy !== null,
|
|
501
|
-
failed: () => Boolean(failure),
|
|
502
|
-
onEvent: (event) => {
|
|
503
|
-
void terminalCall('notice', { kind: 'notify', level: 'info', extension: STOP_GATE_EXTENSION, text: stopGateNotice(event) }).catch(() => undefined);
|
|
504
|
-
}
|
|
505
|
-
})(pi);
|
|
506
|
-
// A /goal (ADR-0072): while one is active, a reply that would end continues in place. After the
|
|
507
|
-
// stop gate, so open todo items are nudged first; its claim check is the only completion check.
|
|
491
|
+
// The stop gate is gone (ADR-0075): a reply the model ends settles as it ends, and the todo list
|
|
492
|
+
// is progress, not a gate. A /goal (ADR-0072) is the only layer that continues a reply: while one
|
|
493
|
+
// is active, a reply that would end continues in place; its claim check is the only completion
|
|
494
|
+
// check.
|
|
508
495
|
if (goal) {
|
|
509
496
|
for (const spec of goalTools(() => goal))
|
|
510
497
|
pi.registerTool({
|
|
@@ -556,8 +543,8 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
556
543
|
}
|
|
557
544
|
})(pi);
|
|
558
545
|
}
|
|
559
|
-
//
|
|
560
|
-
//
|
|
546
|
+
// A reply that did work and reached its budget continues as the next slice of the same run
|
|
547
|
+
// (ADR-0070).
|
|
561
548
|
if (slicePolicy.enabled)
|
|
562
549
|
sliceContinuation({
|
|
563
550
|
policy: slicePolicy,
|
|
@@ -566,11 +553,11 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
566
553
|
failed: () => Boolean(failure),
|
|
567
554
|
blocked: () => goal?.get()?.status === 'blocked',
|
|
568
555
|
goalActive: () => goal?.active() === true,
|
|
569
|
-
todo: () =>
|
|
570
|
-
next: () => handoffFields(
|
|
556
|
+
todo: () => lastTodo,
|
|
557
|
+
next: () => handoffFields(lastTodo, goal?.get() ?? null).next,
|
|
571
558
|
onContinue: (ended) => {
|
|
572
|
-
void terminalCall('checkpoint', { summary: `Work slice ${ended.slice} reached its limit after ${ended.steps} steps; the reply continues.`, ...handoffFields(
|
|
573
|
-
void terminalCall('notice', { kind: 'notify', level: 'info', extension:
|
|
559
|
+
void terminalCall('checkpoint', { summary: `Work slice ${ended.slice} reached its limit after ${ended.steps} steps; the reply continues.`, ...handoffFields(lastTodo, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: ended.slice, reason: 'budget' }).catch(() => undefined);
|
|
560
|
+
void terminalCall('notice', { kind: 'notify', level: 'info', extension: WORK_SLICE_EXTENSION, text: `Work slice ${ended.slice} reached ${ended.limit ?? 'its limit'}; continuing with slice ${ended.slice + 1} of up to ${slicePolicy.maxSlices}.` }).catch(() => undefined);
|
|
574
561
|
}
|
|
575
562
|
})(pi);
|
|
576
563
|
// Reply text as it streams, batched, for a paired phone (remote control). The completed message
|
|
@@ -777,16 +764,16 @@ export default async function (pi, planRun, getSession, goal) {
|
|
|
777
764
|
subagentParent = false;
|
|
778
765
|
// A reply that stopped at its work-slice boundary is not a completed task: the record says so,
|
|
779
766
|
// so a host can offer the continuation instead of treating it as done (PROP-0041).
|
|
780
|
-
//
|
|
781
|
-
//
|
|
767
|
+
// An ordinary reply settles as it ends (ADR-0075); only a goal that was blocked, paused or hit
|
|
768
|
+
// a limit stops it for the person, and the checkpoint carries what is left.
|
|
782
769
|
const aborted = event.aborted || messageAborted;
|
|
783
|
-
const stop = !sliceState.endedBy && !failure && !aborted && !planRun?.active ?
|
|
770
|
+
const stop = !sliceState.endedBy && !failure && !aborted && !planRun?.active ? goalStopOutcome(lastTodo, goal?.get() ?? null) : null;
|
|
784
771
|
if (stop) {
|
|
785
|
-
await terminalCall('checkpoint', { summary: stopSummary(stop), ...handoffFields(
|
|
772
|
+
await terminalCall('checkpoint', { summary: stopSummary(stop), ...handoffFields(lastTodo, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: stopCheckpointReason(stop) }).catch(() => undefined);
|
|
786
773
|
}
|
|
787
774
|
else if (sliceState.endedBy && !failure && !aborted) {
|
|
788
775
|
// The guard ended the reply and promised the model the harness would keep its state.
|
|
789
|
-
await terminalCall('checkpoint', { summary: `The reply stopped after ${sliceState.steps} steps at its work-slice boundary.`, ...handoffFields(
|
|
776
|
+
await terminalCall('checkpoint', { summary: `The reply stopped after ${sliceState.steps} steps at its work-slice boundary.`, ...handoffFields(lastTodo, goal?.get() ?? null), files: [], checks: [], decisions: [], slice: sliceState.slice, reason: sliceState.endedBy }).catch(() => undefined);
|
|
790
777
|
}
|
|
791
778
|
const reason = sliceState.endedBy ? 'slice_complete' : stop?.code;
|
|
792
779
|
const code = sliceState.endedBy === 'compaction-thrash' ? 'compaction_thrash' : sliceState.endedBy === 'budget' ? 'slice_budget' : stop?.code;
|
|
@@ -1217,7 +1217,7 @@ export class Terminals {
|
|
|
1217
1217
|
const launchSections = profiles ? [...(input.launch?.sections ?? []).filter((section) => section.id !== profiles.id), profiles] : input.launch?.sections;
|
|
1218
1218
|
// A terminal is a person's own session: the same standard sections a host's session has.
|
|
1219
1219
|
const standard = input.launch?.standard === null ? null : input.launch?.standard ?? { cwd: input.cwd };
|
|
1220
|
-
const pack = compile(env, caller, { ...input.launch, standard, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) } : variant);
|
|
1220
|
+
const pack = compile(env, caller, { ...input.launch, model: input.model ?? input.launch?.model ?? null, standard, sections: launchSections, tools: launchTools }, variant && profiles ? { ...variant, sections: variant.sections.filter((section) => section.id !== profiles.id) } : variant);
|
|
1221
1221
|
record(root, pack);
|
|
1222
1222
|
mkdirSync(this.historyDir(input), { recursive: true });
|
|
1223
1223
|
saveForSession(input.sessionFolder, pack);
|
|
@@ -19,7 +19,7 @@ export function askUserTool(run) {
|
|
|
19
19
|
name: ASK_USER,
|
|
20
20
|
label: 'Ask the user',
|
|
21
21
|
description: 'Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 1–2 word label and aim to keep each question to 25 words or fewer. ' +
|
|
22
|
-
'Every question must offer 1–6 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text “Something else” answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision the request and the files do not answer, not for small talk and never to ask permission for work inside the request. If no answer arrives, continue with your best judgment instead of asking again or treating the turn as blocked.',
|
|
22
|
+
'Every question must offer 1–6 short choices, each with a helpful description of 25 words or fewer, and at least one choice must be marked recommended: true. Recommendations are visual only: they never preselect an answer, and submitted choices keep their original labels. Mark at most one recommended choice for a single-choice question, or several when multiSelect is true. Every question also includes a free-text “Something else” answer. Set multiSelect when several can apply. When a UI diagram helps compare choices, set uiSketch: true and give every choice a detailed, bounded ASCII wireframe. Use ASCII tables and panel borders to show the affected element with relevant neighboring UI, readable labels and spacing. Keep each panel’s outer `+` corners in fixed columns, include side edges at those same columns on every content row, and run horizontal borders corner to corner; if a title sits in a top border, leave at least two dashes on both sides. Keep framing consistent across alternatives and concentrate detail on what changes. Do not substitute a symbolic logo or one-line illustration. The preview supplies its outer border; draw internal borders only for actual UI panels. Use it when you need a decision the request and the files do not answer, not for small talk and never to ask permission for work inside the request. If no answer arrives, continue with your best judgment instead of asking again or treating the turn as blocked. Never use it to ask for a login, a secret or a credential: give the exact command in the reply instead.',
|
|
23
23
|
parameters: (Type) => Type.Object({
|
|
24
24
|
questions: Type.Array(Type.Object({
|
|
25
25
|
summary: Type.String({ description: 'A concise label of one or two words' }),
|
package/dist/sdk/tools/todo.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { todoItemSchema, MAX_TODO_ITEMS } from "../sessions/contracts.js";
|
|
4
4
|
export const TODO_WRITE = 'todo_write';
|
|
5
5
|
/** The trigger the prompt and the tool description share, so progress guidance cannot drift. */
|
|
6
|
-
export const TODO_GUIDANCE = 'If the work has several distinct steps
|
|
7
|
-
'The list shows the person your progress; it is not the scope of the work. The request, and any document it points to, is the scope: add steps as the work shows them, list the steps of a handoff or plan instead of folding them into one "remaining work" item
|
|
8
|
-
'Keep the list true
|
|
6
|
+
export const TODO_GUIDANCE = 'If the work has several distinct steps, call todo_write before your first other tool call with the steps you know so far, and keep the list current as steps start and finish. ' +
|
|
7
|
+
'The list shows the person your progress; it is not the scope of the work. The request, and any document it points to, is the scope: add steps as the work shows them, and list the steps of a handoff or plan instead of folding them into one "remaining work" item. ' +
|
|
8
|
+
'Keep the list true: mark a step completed only once it is done and checked, and never mark unfinished work completed. Before you end the turn, compare the request, not the list, with what is done and checked; if a step stays open, say why in the reply. ' +
|
|
9
|
+
'When a login blocks a step, leave it open and give the exact command the person must run in the reply. ' +
|
|
9
10
|
'The person follows your work from it, and a subagent’s todo list shows as its progress in the person’s list of subagents.';
|
|
10
11
|
/** The event a run writes; `checklist.updated` is the pre-rename name, still read for old runs. */
|
|
11
12
|
const TODO_EVENTS = new Set(['todo.updated', 'checklist.updated']);
|