@xingular/harness 1.3.1-unstable.8 → 1.4.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 +122 -5
- package/NOTICE.md +26 -0
- package/builtin/skills/plugin-authoring/SKILL.md +20 -1
- package/dist/cli.js +2424 -1237
- package/dist/sdk/accounts/accounts.d.ts +26 -20
- package/dist/sdk/accounts/accounts.js +100 -63
- package/dist/sdk/agents/builtin.js +2 -0
- package/dist/sdk/commands/registry.d.ts +7 -0
- package/dist/sdk/commands/registry.js +1 -0
- package/dist/sdk/core/commandBudget.js +7 -2
- package/dist/sdk/core/workspaceWriteOperations.d.ts +24 -0
- package/dist/sdk/core/workspaceWriteOperations.js +21 -0
- package/dist/sdk/plugins/builtin.js +0 -4
- package/dist/sdk/plugins/discover.js +80 -4
- package/dist/sdk/plugins/install.js +26 -0
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +20 -1
- package/dist/sdk/prompt/compile.d.ts +4 -1
- package/dist/sdk/prompt/compile.js +8 -21
- package/dist/sdk/prompt/reporting.d.ts +2 -0
- package/dist/sdk/prompt/reporting.js +282 -0
- package/dist/sdk/prompt/standard.d.ts +12 -0
- package/dist/sdk/prompt/standard.js +53 -2
- package/dist/sdk/sessions/checkpoint.d.ts +7 -0
- package/dist/sdk/sessions/checkpoint.js +41 -0
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +2 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +2 -1
- package/dist/sdk/sessions/completionCheck.d.ts +49 -0
- package/dist/sdk/sessions/completionCheck.js +143 -0
- package/dist/sdk/sessions/continuation.js +1 -1
- package/dist/sdk/sessions/contracts.d.ts +97 -1
- package/dist/sdk/sessions/contracts.js +17 -0
- package/dist/sdk/sessions/goal.d.ts +145 -0
- package/dist/sdk/sessions/goal.js +405 -0
- package/dist/sdk/sessions/goalLoop.d.ts +37 -0
- package/dist/sdk/sessions/goalLoop.js +125 -0
- package/dist/sdk/sessions/goalPrompts.d.ts +4 -0
- package/dist/sdk/sessions/goalPrompts.js +93 -0
- package/dist/sdk/sessions/planMode.d.ts +31 -0
- package/dist/sdk/sessions/planMode.js +48 -3
- package/dist/sdk/sessions/registry.d.ts +6 -1
- package/dist/sdk/sessions/registry.js +4 -4
- package/dist/sdk/sessions/runStore.js +1 -0
- package/dist/sdk/sessions/runner.d.ts +9 -0
- package/dist/sdk/sessions/runner.js +65 -13
- package/dist/sdk/sessions/sessions.d.ts +10 -0
- package/dist/sdk/sessions/sessions.js +87 -10
- package/dist/sdk/sessions/stopGate.d.ts +78 -0
- package/dist/sdk/sessions/stopGate.js +143 -0
- package/dist/sdk/sessions/subagentService.d.ts +2 -0
- package/dist/sdk/sessions/subagentService.js +1 -1
- package/dist/sdk/sessions/workSlice.d.ts +61 -12
- package/dist/sdk/sessions/workSlice.js +102 -33
- package/dist/sdk/terminal/controller.d.ts +2 -2
- package/dist/sdk/terminal/controller.js +14 -4
- package/dist/sdk/terminal/credentialStore.d.ts +4 -0
- package/dist/sdk/terminal/credentialStore.js +33 -0
- package/dist/sdk/terminal/piExtension.d.ts +2 -1
- package/dist/sdk/terminal/piExtension.js +160 -27
- package/dist/sdk/terminal/terminals.js +27 -4
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +3 -0
- package/dist/sdk/terminal/tui/goalCommand.d.ts +19 -0
- package/dist/sdk/terminal/tui/goalCommand.js +83 -0
- package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
- package/dist/sdk/terminal/tui/planMode.js +1 -1
- package/dist/sdk/terminal/tui/runtime.d.ts +3 -0
- package/dist/sdk/terminal/tui/runtime.js +21 -4
- package/dist/sdk/tools/askUser.js +1 -1
- package/dist/sdk/tools/todo.js +2 -1
- package/dist/terminal-opentui.js +1740 -627
- package/dist/terminal-pi.js +996 -407
- package/docs/architecture/02-sessions-and-runs.md +47 -11
- package/docs/architecture/04-prompt.md +83 -16
- package/docs/architecture/06-plugins-and-capabilities.md +16 -1
- package/docs/architecture/07-tools-and-gate.md +7 -2
- package/docs/architecture/09-accounts-and-connectors.md +9 -0
- package/docs/architecture/10-server-protocol.md +13 -0
- package/docs/architecture/11-terminal.md +38 -12
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +2 -0
- package/docs/decisions/ADR-0008-flexible-reporting-guidance.md +3 -0
- package/docs/decisions/ADR-0063-unfinished-work-handoffs.md +2 -0
- package/docs/decisions/ADR-0068-scope-and-completion-report.md +60 -0
- package/docs/decisions/ADR-0069-stop-gate-and-finish.md +130 -0
- package/docs/decisions/ADR-0070-terminal-slice-continuation.md +46 -0
- package/docs/decisions/ADR-0071-completion-check.md +104 -0
- package/docs/decisions/ADR-0072-codex-prompts-and-goal.md +105 -0
- package/docs/decisions/ADR-0073-large-reporting-prompt.md +84 -0
- package/docs/parity.md +4 -1
- package/docs/releases/1.3.1-unstable.9.md +54 -0
- package/docs/releases/1.4.0-unstable.md +18 -0
- package/docs/releases/README.md +1 -1
- package/docs/study/00-index.md +10 -0
- package/docs/study/STUDY-0004-turn-end-controls.md +123 -0
- package/docs/study/STUDY-0005-skill-installation-guidance.md +146 -0
- package/docs/study/STUDY-0006-reporting-prompts.md +81 -0
- package/docs/study/arms/04-skills.md +6 -0
- package/docs/terminal-tui.md +17 -7
- package/package.json +1 -1
- package/dist/sdk/tools/checkpoint.d.ts +0 -27
- package/dist/sdk/tools/checkpoint.js +0 -154
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import { type CheckModel } from "./completionCheck.js";
|
|
3
|
+
import { type Checkpoint, type TodoItem } from "./contracts.js";
|
|
4
|
+
import type { GoalState } from "./goal.js";
|
|
5
|
+
export declare const STOP_GATE_EXTENSION = "stop-gate";
|
|
6
|
+
export declare const STOP_GATE_CUSTOM_TYPE = "xingular-stop-gate";
|
|
7
|
+
/** Nudges per reply before the reply is handed back to the person. */
|
|
8
|
+
export declare const MAX_STOP_GATE_NUDGES = 3;
|
|
9
|
+
/** What the gate knows about the live reply. */
|
|
10
|
+
export interface StopGateState {
|
|
11
|
+
/** The reply's todo list as the model last wrote it (or as a continued run inherited it). */
|
|
12
|
+
todo: TodoItem[];
|
|
13
|
+
/** The person's request this reply answers. */
|
|
14
|
+
request: string;
|
|
15
|
+
/** Nudges sent in this reply. */
|
|
16
|
+
nudges: number;
|
|
17
|
+
/** Work tool calls since the last nudge. */
|
|
18
|
+
callsSinceNudge: number;
|
|
19
|
+
/** Work tool calls in this reply. */
|
|
20
|
+
callsInReply: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function createStopGateState(initialTodo?: readonly TodoItem[]): StopGateState;
|
|
23
|
+
/** A new reply starts from the inherited todo list, with no nudges. */
|
|
24
|
+
export declare function resetStopGateState(state: StopGateState, initialTodo?: readonly TodoItem[], request?: string): void;
|
|
25
|
+
/** The todo items a reply has not finished. */
|
|
26
|
+
export declare function openItems(todo: readonly TodoItem[]): TodoItem[];
|
|
27
|
+
/** How a settled reply ended when its work is not done; `null` when it is. */
|
|
28
|
+
export type StopOutcome = {
|
|
29
|
+
code: 'needs_person';
|
|
30
|
+
reason: string;
|
|
31
|
+
open: TodoItem[];
|
|
32
|
+
} | {
|
|
33
|
+
code: 'unfinished';
|
|
34
|
+
reason: string;
|
|
35
|
+
open: TodoItem[];
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* `goal` is the session's /goal, when it has one: a goal that ended `blocked` needs the person,
|
|
39
|
+
* and one that ended on a limit, a pause or a stall leaves work (ADR-0072).
|
|
40
|
+
*/
|
|
41
|
+
export declare function stopOutcome(state: StopGateState, goal?: GoalState | null): StopOutcome | null;
|
|
42
|
+
/** What a checkpoint learns from the reply's todo list and the goal. */
|
|
43
|
+
export declare function handoffFields(state?: StopGateState | null, goal?: GoalState | null): Pick<Checkpoint, 'done' | 'next'> & {
|
|
44
|
+
blocked?: string;
|
|
45
|
+
};
|
|
46
|
+
/** Why a checkpoint written for a stop outcome was written. */
|
|
47
|
+
export declare const stopCheckpointReason: (outcome: StopOutcome) => Checkpoint["reason"];
|
|
48
|
+
/** The summary a stop outcome's checkpoint falls back to when the reply left no text. */
|
|
49
|
+
export declare function stopSummary(outcome: StopOutcome): string;
|
|
50
|
+
/** The text the model reads when the todo gate sends its reply back. */
|
|
51
|
+
export declare function nudgeText(count: number, max: number, open: readonly TodoItem[]): string;
|
|
52
|
+
/** What the gate reports for the run record: each nudge. */
|
|
53
|
+
export type StopGateEvent = {
|
|
54
|
+
kind: 'todo';
|
|
55
|
+
count: number;
|
|
56
|
+
open: TodoItem[];
|
|
57
|
+
};
|
|
58
|
+
/** One line for the run record per stop-gate nudge. */
|
|
59
|
+
export declare function stopGateNotice(event: StopGateEvent): string;
|
|
60
|
+
export interface StopGateOptions {
|
|
61
|
+
state: StopGateState;
|
|
62
|
+
/** The todo list a continued run inherits; each new reply starts from it. */
|
|
63
|
+
initialTodo?: readonly TodoItem[];
|
|
64
|
+
/** The person's request when the prompt is not it (a continuation's "Continue from where you stopped."). */
|
|
65
|
+
request?: () => string | null;
|
|
66
|
+
/** Whether the gate applies to the current reply; a function follows the terminal's live plan switch. */
|
|
67
|
+
active?: () => boolean;
|
|
68
|
+
/** True when the work-slice guard ended this reply: the gate stays silent and the slice hands off. */
|
|
69
|
+
sliceEnded?: () => boolean;
|
|
70
|
+
/** True when the reply already failed (a provider error): nothing to continue. */
|
|
71
|
+
failed?: () => boolean;
|
|
72
|
+
max?: number;
|
|
73
|
+
/** Called for each nudge, for the run record. */
|
|
74
|
+
onEvent?: (event: StopGateEvent) => void;
|
|
75
|
+
}
|
|
76
|
+
export declare function stopGate(options: StopGateOptions): (pi: ExtensionAPI) => void;
|
|
77
|
+
/** The model a goal-claim check asks: the session's own model, at low effort, through Pi's registry. */
|
|
78
|
+
export declare function sessionCheckModel(ctx: unknown): CheckModel | null;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { TODO_WRITE } from "../tools/todo.js";
|
|
2
|
+
import { validatedTodo } from "./compactionCheckpoint.js";
|
|
3
|
+
import { answerText } from "./completionCheck.js";
|
|
4
|
+
import { MAX_CHECKPOINT_DONE } from "./contracts.js";
|
|
5
|
+
export const STOP_GATE_EXTENSION = 'stop-gate';
|
|
6
|
+
export const STOP_GATE_CUSTOM_TYPE = 'xingular-stop-gate';
|
|
7
|
+
/** Nudges per reply before the reply is handed back to the person. */
|
|
8
|
+
export const MAX_STOP_GATE_NUDGES = 3;
|
|
9
|
+
/** Calls that are bookkeeping, not work: they neither count as progress nor make a reply checkable. */
|
|
10
|
+
const BOOKKEEPING_TOOLS = new Set([TODO_WRITE, 'session_name']);
|
|
11
|
+
export function createStopGateState(initialTodo = []) {
|
|
12
|
+
return { todo: validatedTodo(initialTodo) ?? [], request: '', nudges: 0, callsSinceNudge: 0, callsInReply: 0 };
|
|
13
|
+
}
|
|
14
|
+
/** A new reply starts from the inherited todo list, with no nudges. */
|
|
15
|
+
export function resetStopGateState(state, initialTodo = [], request = '') {
|
|
16
|
+
state.todo = validatedTodo(initialTodo) ?? [];
|
|
17
|
+
state.request = request;
|
|
18
|
+
state.nudges = 0;
|
|
19
|
+
state.callsSinceNudge = 0;
|
|
20
|
+
state.callsInReply = 0;
|
|
21
|
+
}
|
|
22
|
+
/** The todo items a reply has not finished. */
|
|
23
|
+
export function openItems(todo) {
|
|
24
|
+
return todo.filter((item) => item.status !== 'completed');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `goal` is the session's /goal, when it has one: a goal that ended `blocked` needs the person,
|
|
28
|
+
* and one that ended on a limit, a pause or a stall leaves work (ADR-0072).
|
|
29
|
+
*/
|
|
30
|
+
export function stopOutcome(state, goal) {
|
|
31
|
+
const open = openItems(state.todo);
|
|
32
|
+
if (goal?.status === 'blocked')
|
|
33
|
+
return { code: 'needs_person', reason: goal.lastReason || 'The goal is blocked.', open };
|
|
34
|
+
if (goal && (goal.status === 'budget_limited' || goal.status === 'usage_limited' || goal.status === 'paused')) {
|
|
35
|
+
return { code: 'unfinished', reason: goal.lastReason || `The goal is ${goal.status.replace('_', ' ')}.`, open };
|
|
36
|
+
}
|
|
37
|
+
if (open.length)
|
|
38
|
+
return { code: 'unfinished', reason: '', open };
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/** What a checkpoint learns from the reply's todo list and the goal. */
|
|
42
|
+
export function handoffFields(state, goal) {
|
|
43
|
+
const todo = state?.todo ?? [];
|
|
44
|
+
const next = openItems(todo)[0]?.text || '';
|
|
45
|
+
const blocked = goal?.status === 'blocked' ? (goal.lastReason || 'The goal is blocked.').slice(0, 500) : '';
|
|
46
|
+
return {
|
|
47
|
+
done: todo.filter((item) => item.status === 'completed').slice(0, MAX_CHECKPOINT_DONE).map((item) => ({ item: item.text.slice(0, 300), evidence: [] })),
|
|
48
|
+
next: next.slice(0, 500),
|
|
49
|
+
...(blocked ? { blocked } : {})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Why a checkpoint written for a stop outcome was written. */
|
|
53
|
+
export const stopCheckpointReason = (outcome) => outcome.code === 'needs_person' ? 'requested' : 'stuck';
|
|
54
|
+
/** The summary a stop outcome's checkpoint falls back to when the reply left no text. */
|
|
55
|
+
export function stopSummary(outcome) {
|
|
56
|
+
if (outcome.code === 'needs_person')
|
|
57
|
+
return `The reply stopped for the person: ${outcome.reason}`.slice(0, 1_200);
|
|
58
|
+
if (outcome.reason)
|
|
59
|
+
return `The reply ended with work left: ${outcome.reason}`.slice(0, 1_200);
|
|
60
|
+
return `The reply ended with ${outcome.open.length} open todo item${outcome.open.length === 1 ? '' : 's'}.`;
|
|
61
|
+
}
|
|
62
|
+
/** The text the model reads when the todo gate sends its reply back. */
|
|
63
|
+
export function nudgeText(count, max, open) {
|
|
64
|
+
const list = open.slice(0, 12).map((item) => `- ${item.text}`).join('\n');
|
|
65
|
+
const more = open.length > 12 ? `\n- …and ${open.length - 12} more` : '';
|
|
66
|
+
return [
|
|
67
|
+
`Harness stop gate (${count}/${max}): your reply ended with ${open.length} open todo item${open.length === 1 ? '' : 's'}:`,
|
|
68
|
+
`${list}${more}`,
|
|
69
|
+
'Continue with the next open item now. Mark an item completed only after it is done and checked.',
|
|
70
|
+
'The list shows progress; it is not the limit of the work. If the request needs more than the list says, add it and do it.',
|
|
71
|
+
'Decisions inside the request are yours. Stop only for something only the person can do (a deploy, a secret, remote data, money, missing access), and then say exactly what it is.'
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
|
74
|
+
/** One line for the run record per stop-gate nudge. */
|
|
75
|
+
export function stopGateNotice(event) {
|
|
76
|
+
return `Stop gate ${event.count}: the reply ended with ${event.open.length} open todo item${event.open.length === 1 ? '' : 's'}; continuing.`;
|
|
77
|
+
}
|
|
78
|
+
export function stopGate(options) {
|
|
79
|
+
const { state } = options;
|
|
80
|
+
const max = options.max ?? MAX_STOP_GATE_NUDGES;
|
|
81
|
+
return (pi) => {
|
|
82
|
+
// Pi emits agent_start again for every continuation, so a reply resets only on a person's message.
|
|
83
|
+
pi.on('before_agent_start', (event) => {
|
|
84
|
+
if (event.messageType !== undefined && event.messageType !== 'user')
|
|
85
|
+
return;
|
|
86
|
+
resetStopGateState(state, options.initialTodo, options.request?.() || (typeof event.prompt === 'string' ? event.prompt : ''));
|
|
87
|
+
});
|
|
88
|
+
pi.on('tool_call', (event) => {
|
|
89
|
+
if (BOOKKEEPING_TOOLS.has(event.toolName))
|
|
90
|
+
return;
|
|
91
|
+
state.callsSinceNudge += 1;
|
|
92
|
+
state.callsInReply += 1;
|
|
93
|
+
});
|
|
94
|
+
pi.on('tool_result', (event) => {
|
|
95
|
+
if (event.toolName !== TODO_WRITE || event.isError)
|
|
96
|
+
return;
|
|
97
|
+
const items = validatedTodo(event.input?.items);
|
|
98
|
+
if (items)
|
|
99
|
+
state.todo = items;
|
|
100
|
+
});
|
|
101
|
+
pi.on('agent_before_settle', (event, ctx) => {
|
|
102
|
+
if (event.outcome !== 'completed' || event.continue || ctx?.signal?.aborted)
|
|
103
|
+
return;
|
|
104
|
+
if (options.active && !options.active())
|
|
105
|
+
return;
|
|
106
|
+
if (options.sliceEnded?.() || options.failed?.())
|
|
107
|
+
return;
|
|
108
|
+
if (state.nudges >= max)
|
|
109
|
+
return;
|
|
110
|
+
// A nudge that produced no work will not be followed by one that does: hand it back.
|
|
111
|
+
if (state.nudges > 0 && state.callsSinceNudge === 0)
|
|
112
|
+
return;
|
|
113
|
+
const open = openItems(state.todo);
|
|
114
|
+
if (!open.length)
|
|
115
|
+
return;
|
|
116
|
+
state.nudges += 1;
|
|
117
|
+
state.callsSinceNudge = 0;
|
|
118
|
+
options.onEvent?.({ kind: 'todo', count: state.nudges, open });
|
|
119
|
+
return {
|
|
120
|
+
entries: [...event.entries, { type: 'custom_message', customType: STOP_GATE_CUSTOM_TYPE, content: nudgeText(state.nudges, max, open), display: true }],
|
|
121
|
+
continue: true
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** The model a goal-claim check asks: the session's own model, at low effort, through Pi's registry. */
|
|
127
|
+
export function sessionCheckModel(ctx) {
|
|
128
|
+
const session = (ctx ?? {});
|
|
129
|
+
const model = session.model;
|
|
130
|
+
const registry = session.modelRegistry;
|
|
131
|
+
if (!model || !registry)
|
|
132
|
+
return null;
|
|
133
|
+
// Some providers (opencode-go) reject a request without a session id.
|
|
134
|
+
const sessionId = session.sessionManager?.getSessionId?.();
|
|
135
|
+
return async (system, user, signal) => {
|
|
136
|
+
const context = { systemPrompt: system, messages: [{ role: 'user', content: user, timestamp: Date.now() }] };
|
|
137
|
+
const message = await registry.streamSimple(model, context, { ...(model.reasoning ? { reasoning: 'low' } : {}), ...(sessionId ? { sessionId } : {}), maxTokens: 4_000, signal }).result();
|
|
138
|
+
const failed = message.stopReason;
|
|
139
|
+
if (failed === 'error' || failed === 'aborted')
|
|
140
|
+
throw new Error(message.errorMessage || `the check request ended: ${failed}`);
|
|
141
|
+
return { text: answerText(message), message };
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -11,8 +11,10 @@ type Started = {
|
|
|
11
11
|
nativeSessionId: string | null;
|
|
12
12
|
};
|
|
13
13
|
/** What a spawn returns: the child, the profile it started from, and profile tools missing here. */
|
|
14
|
+
/** `tools` is what the child can actually use, so the parent sees a brief/tool mismatch at once. */
|
|
14
15
|
export type SpawnedSubagent = Started & {
|
|
15
16
|
agent?: string;
|
|
17
|
+
tools?: string[];
|
|
16
18
|
unavailableTools?: string[];
|
|
17
19
|
continued?: string;
|
|
18
20
|
};
|
|
@@ -232,7 +232,7 @@ export class Subagents {
|
|
|
232
232
|
...(input.plan ? { connections: false, subagents: null } : {})
|
|
233
233
|
};
|
|
234
234
|
const result = await this.launchChild(parent, request, child);
|
|
235
|
-
return { ...result, ...(profile ? { agent: profile.name } : {}), ...(narrowed.unavailable.length ? { unavailableTools: narrowed.unavailable } : {}) };
|
|
235
|
+
return { ...result, ...(profile ? { agent: profile.name } : {}), tools, ...(narrowed.unavailable.length ? { unavailableTools: narrowed.unavailable } : {}) };
|
|
236
236
|
}
|
|
237
237
|
finally {
|
|
238
238
|
this.release(request.parentRunId, runId);
|
|
@@ -41,32 +41,35 @@ export interface SliceState {
|
|
|
41
41
|
startedAt: number;
|
|
42
42
|
lastTool: string | null;
|
|
43
43
|
compactions: number;
|
|
44
|
-
checkpointWritten: boolean;
|
|
45
44
|
/** Set when the guard ended the reply instead of the model finishing it. */
|
|
46
45
|
endedBy: SliceEnd | null;
|
|
47
46
|
/** The limit that ended it, for the record. */
|
|
48
47
|
endedByLimit: string | null;
|
|
49
48
|
/** Steps past which every non-exempt tool call is refused, once one was refused. */
|
|
50
49
|
refusedAt: number | null;
|
|
50
|
+
/** Whether this slice did work: a successful command or file change. A fruitless slice is not continued. */
|
|
51
|
+
progressed: boolean;
|
|
51
52
|
}
|
|
52
53
|
export declare function createSliceState(_policy: SlicePolicy, slice?: number, now?: number): SliceState;
|
|
53
54
|
/** Counters start again for each reply; a terminal session keeps one guard across replies. */
|
|
54
55
|
export declare function resetSliceState(state: SliceState, now?: number): void;
|
|
56
|
+
/**
|
|
57
|
+
* The next slice of the same reply: fresh counters and clock, one more slice. Compactions carry
|
|
58
|
+
* over, so a reply that keeps compacting still ends.
|
|
59
|
+
*/
|
|
60
|
+
export declare function beginNextSlice(state: SliceState, now?: number): void;
|
|
55
61
|
/** The first limit this reply has passed, named as a person would say it. */
|
|
56
62
|
export declare function sliceLimitReached(policy: SlicePolicy, state: SliceState, now?: number): string | null;
|
|
57
63
|
interface ToolCall {
|
|
58
64
|
toolCallId: string;
|
|
59
65
|
toolName: string;
|
|
60
66
|
}
|
|
61
|
-
interface ToolResult {
|
|
62
|
-
toolName: string;
|
|
63
|
-
isError?: boolean;
|
|
64
|
-
}
|
|
65
67
|
/** The part of Pi's extension API this guard uses. Structural, so the module needs no Pi import. */
|
|
66
68
|
interface GuardApi {
|
|
67
|
-
on(event: '
|
|
69
|
+
on(event: 'before_agent_start', handler: (event: {
|
|
70
|
+
messageType?: string;
|
|
71
|
+
}) => unknown): void;
|
|
68
72
|
on(event: 'tool_call', handler: (event: ToolCall) => unknown): void;
|
|
69
|
-
on(event: 'tool_result', handler: (event: ToolResult) => unknown): void;
|
|
70
73
|
on(event: 'message_end', handler: (event: {
|
|
71
74
|
message: {
|
|
72
75
|
role: string;
|
|
@@ -82,18 +85,64 @@ interface GuardApi {
|
|
|
82
85
|
export interface WorkSliceGuardOptions {
|
|
83
86
|
policy: SlicePolicy;
|
|
84
87
|
state: SliceState;
|
|
85
|
-
/** Whether checkpoint_write is active; a function supports the terminal's live plan-mode switch. */
|
|
86
|
-
checkpointAvailable?: boolean | (() => boolean);
|
|
87
88
|
/** Called when a triggering call terminates the reply; the runner ends it. */
|
|
88
89
|
onEnd?: (end: SliceEnd, limit: string) => void;
|
|
89
90
|
onRefuse?: (event: ToolCall, reason: string) => void;
|
|
90
91
|
}
|
|
91
92
|
/**
|
|
92
|
-
* Bounds one reply. A call past the budget is refused
|
|
93
|
-
*
|
|
94
|
-
*
|
|
93
|
+
* Bounds one reply. A call past the budget is refused with one wrap-up request, and an ended reply
|
|
94
|
+
* lets only a handoff call through. Three compactions in one reply end it too. Every other call is
|
|
95
|
+
* allowed, however often it is made.
|
|
95
96
|
*/
|
|
96
97
|
export declare function workSliceGuard(options: WorkSliceGuardOptions): (pi: GuardApi) => void;
|
|
98
|
+
export declare const SLICE_CONTINUE_CUSTOM_TYPE = "xingular-slice-continue";
|
|
99
|
+
/** The part of Pi's extension API the continuation uses. Structural, so the module needs no Pi import. */
|
|
100
|
+
interface ContinuationApi {
|
|
101
|
+
on(event: 'before_agent_start', handler: (event: {
|
|
102
|
+
messageType?: string;
|
|
103
|
+
}) => unknown): void;
|
|
104
|
+
on(event: 'tool_result', handler: (event: {
|
|
105
|
+
toolName: string;
|
|
106
|
+
isError?: boolean;
|
|
107
|
+
}) => unknown): void;
|
|
108
|
+
on(event: 'agent_before_settle', handler: (event: {
|
|
109
|
+
outcome?: string;
|
|
110
|
+
continue?: boolean;
|
|
111
|
+
entries: unknown[];
|
|
112
|
+
}, ctx?: {
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
}) => unknown): void;
|
|
115
|
+
}
|
|
116
|
+
export interface SliceContinuationOptions {
|
|
117
|
+
policy: SlicePolicy;
|
|
118
|
+
state: SliceState;
|
|
119
|
+
/** False while a plan runs: a plan is not continued. */
|
|
120
|
+
active?: () => boolean;
|
|
121
|
+
failed?: () => boolean;
|
|
122
|
+
/** True when the completion check said the reply needs the person (ADR-0071). */
|
|
123
|
+
blocked?: () => boolean;
|
|
124
|
+
/** The todo list: when every item is done, the work is not continued. */
|
|
125
|
+
todo?: () => readonly {
|
|
126
|
+
status: string;
|
|
127
|
+
}[];
|
|
128
|
+
/** The next open step, for the continue message. */
|
|
129
|
+
next?: () => string;
|
|
130
|
+
/** True while a /goal is active: a goal's slices go on even when every todo item is done (ADR-0072). */
|
|
131
|
+
goalActive?: () => boolean;
|
|
132
|
+
/** Called before the next slice starts, with the slice that ended. */
|
|
133
|
+
onContinue?: (ended: {
|
|
134
|
+
slice: number;
|
|
135
|
+
steps: number;
|
|
136
|
+
limit: string | null;
|
|
137
|
+
}) => void;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Continues a reply in place when its slice ended at the budget (PROP-0041, ADR-0070): the same
|
|
141
|
+
* run and the same cached prompt, with fresh counters, up to `maxSlices`. Never after a fruitless,
|
|
142
|
+
* blocked, failed, aborted or plan slice, or when every todo item is done. A person's message
|
|
143
|
+
* starts again at slice 1.
|
|
144
|
+
*/
|
|
145
|
+
export declare function sliceContinuation(options: SliceContinuationOptions): (pi: ContinuationApi) => void;
|
|
97
146
|
/**
|
|
98
147
|
* Which slice of its session a run is, from 1. Counts the recorded continuation chain, so it
|
|
99
148
|
* survives a restart and needs no extra field on the run record.
|
|
@@ -23,6 +23,17 @@ const LIMITS = {
|
|
|
23
23
|
};
|
|
24
24
|
export class WorkSliceError extends Error {
|
|
25
25
|
}
|
|
26
|
+
/** `XINGULAR_WORK_SLICE=maxToolCalls=15,maxSlices=3` at launch: the same fields as a run's policy. */
|
|
27
|
+
function launchPolicy(text) {
|
|
28
|
+
const record = {};
|
|
29
|
+
for (const part of text.split(',')) {
|
|
30
|
+
const [key, raw] = part.split('=').map((value) => value.trim());
|
|
31
|
+
if (!key || raw === undefined)
|
|
32
|
+
throw new WorkSliceError(`XINGULAR_WORK_SLICE must be off or key=value pairs, not ${JSON.stringify(text)}.`);
|
|
33
|
+
record[key] = raw === 'true' ? true : raw === 'false' ? false : Number(raw);
|
|
34
|
+
}
|
|
35
|
+
return record;
|
|
36
|
+
}
|
|
26
37
|
/**
|
|
27
38
|
* The policy a run asked for, or the defaults. `XINGULAR_WORK_SLICE=off` at launch turns the guard
|
|
28
39
|
* off for every run in this process; nothing is persisted, so restarting without it restores the
|
|
@@ -30,8 +41,12 @@ export class WorkSliceError extends Error {
|
|
|
30
41
|
*/
|
|
31
42
|
export function parseWorkSlice(input) {
|
|
32
43
|
if (input === undefined || input === null) {
|
|
33
|
-
const launch = process.env.XINGULAR_WORK_SLICE?.trim()
|
|
34
|
-
|
|
44
|
+
const launch = process.env.XINGULAR_WORK_SLICE?.trim();
|
|
45
|
+
if (launch === undefined || launch === '')
|
|
46
|
+
return { ...DEFAULT_SLICE_POLICY };
|
|
47
|
+
if (['off', '0', 'false'].includes(launch.toLowerCase()))
|
|
48
|
+
return { ...DEFAULT_SLICE_POLICY, enabled: false };
|
|
49
|
+
return parseWorkSlice(launchPolicy(launch));
|
|
35
50
|
}
|
|
36
51
|
if (typeof input !== 'object' || Array.isArray(input))
|
|
37
52
|
throw new WorkSliceError('workSlice must be an object.');
|
|
@@ -68,7 +83,7 @@ export function parseWorkSlice(input) {
|
|
|
68
83
|
return policy;
|
|
69
84
|
}
|
|
70
85
|
export function createSliceState(_policy, slice = 1, now = Date.now()) {
|
|
71
|
-
return { slice, steps: 0, tokens: 0, startedAt: now, lastTool: null, compactions: 0,
|
|
86
|
+
return { slice, steps: 0, tokens: 0, startedAt: now, lastTool: null, compactions: 0, endedBy: null, endedByLimit: null, refusedAt: null, progressed: false };
|
|
72
87
|
}
|
|
73
88
|
/** Counters start again for each reply; a terminal session keeps one guard across replies. */
|
|
74
89
|
export function resetSliceState(state, now = Date.now()) {
|
|
@@ -77,10 +92,20 @@ export function resetSliceState(state, now = Date.now()) {
|
|
|
77
92
|
state.startedAt = now;
|
|
78
93
|
state.lastTool = null;
|
|
79
94
|
state.compactions = 0;
|
|
80
|
-
state.checkpointWritten = false;
|
|
81
95
|
state.endedBy = null;
|
|
82
96
|
state.endedByLimit = null;
|
|
83
97
|
state.refusedAt = null;
|
|
98
|
+
state.progressed = false;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The next slice of the same reply: fresh counters and clock, one more slice. Compactions carry
|
|
102
|
+
* over, so a reply that keeps compacting still ends.
|
|
103
|
+
*/
|
|
104
|
+
export function beginNextSlice(state, now = Date.now()) {
|
|
105
|
+
const compactions = state.compactions;
|
|
106
|
+
resetSliceState(state, now);
|
|
107
|
+
state.compactions = compactions;
|
|
108
|
+
state.slice += 1;
|
|
84
109
|
}
|
|
85
110
|
/** The first limit this reply has passed, named as a person would say it. */
|
|
86
111
|
export function sliceLimitReached(policy, state, now = Date.now()) {
|
|
@@ -92,37 +117,40 @@ export function sliceLimitReached(policy, state, now = Date.now()) {
|
|
|
92
117
|
return `${Math.round(policy.maxNewTokens / 1000)}k new tokens`;
|
|
93
118
|
return null;
|
|
94
119
|
}
|
|
95
|
-
/** Tools a reply may still call
|
|
96
|
-
const HANDOFF_TOOLS = new Set(['
|
|
120
|
+
/** Tools a reply may still call after its slice ended. A plan update waits for the next slice. */
|
|
121
|
+
const HANDOFF_TOOLS = new Set(['session_name']);
|
|
97
122
|
const clip = (value, limit) => (value.length <= limit ? value : `${value.slice(0, limit - 1)}…`);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
// The harness writes the handoff itself from the reply's files, checks and todo list (ADR-0069), so
|
|
124
|
+
// the model is only asked to stop and say where it is.
|
|
125
|
+
function handoffRequest(limit, state) {
|
|
126
|
+
return clip(`This reply reached its work-slice limit (${limit}; step ${state.steps}). Stop using tools and write a short report: what is done or found, what is left, and the single next step. ` +
|
|
127
|
+
'The Harness records a checkpoint from this reply\'s evidence, and the next slice continues from it. Do not call other tools.', 600);
|
|
103
128
|
}
|
|
104
129
|
/**
|
|
105
|
-
* Bounds one reply. A call past the budget is refused
|
|
106
|
-
*
|
|
107
|
-
*
|
|
130
|
+
* Bounds one reply. A call past the budget is refused with one wrap-up request, and an ended reply
|
|
131
|
+
* lets only a handoff call through. Three compactions in one reply end it too. Every other call is
|
|
132
|
+
* allowed, however often it is made.
|
|
108
133
|
*/
|
|
109
134
|
export function workSliceGuard(options) {
|
|
110
135
|
const { policy, state } = options;
|
|
111
|
-
const checkpointAvailable = () => typeof options.checkpointAvailable === 'function'
|
|
112
|
-
? options.checkpointAvailable()
|
|
113
|
-
: options.checkpointAvailable !== false;
|
|
114
136
|
const end = (kind, limit) => {
|
|
115
137
|
state.endedBy = kind;
|
|
116
138
|
state.endedByLimit = limit;
|
|
117
139
|
options.onEnd?.(kind, limit);
|
|
118
|
-
const canCheckpoint = checkpointAvailable();
|
|
119
140
|
const reason = kind === 'compaction-thrash'
|
|
120
|
-
? handoffRequest('three context compactions in one reply', state
|
|
121
|
-
: handoffRequest(limit, state
|
|
141
|
+
? handoffRequest('three context compactions in one reply', state)
|
|
142
|
+
: handoffRequest(limit, state);
|
|
122
143
|
return { block: true, terminate: true, reason };
|
|
123
144
|
};
|
|
124
145
|
return (pi) => {
|
|
125
|
-
|
|
146
|
+
// Pi emits agent_start again for every continuation (a stop-gate nudge, delivered subagent
|
|
147
|
+
// results, a plan repair), and those belong to the same reply and the same budget. Only a
|
|
148
|
+
// person's message starts a new slice.
|
|
149
|
+
pi.on('before_agent_start', (event) => {
|
|
150
|
+
if (event.messageType !== undefined && event.messageType !== 'user')
|
|
151
|
+
return;
|
|
152
|
+
resetSliceState(state);
|
|
153
|
+
});
|
|
126
154
|
pi.on('message_end', (event) => {
|
|
127
155
|
const usage = event.message.usage;
|
|
128
156
|
if (event.message.role !== 'assistant' || !usage)
|
|
@@ -130,22 +158,15 @@ export function workSliceGuard(options) {
|
|
|
130
158
|
// New tokens only: the cached prefix was sent before and is not new context or new cost.
|
|
131
159
|
state.tokens += (usage.input ?? 0) + (usage.cacheWrite ?? 0) + (usage.output ?? 0);
|
|
132
160
|
});
|
|
133
|
-
pi.on('tool_result', (event) => {
|
|
134
|
-
if (event.toolName === 'checkpoint_write' && !event.isError)
|
|
135
|
-
state.checkpointWritten = true;
|
|
136
|
-
});
|
|
137
161
|
pi.on('tool_call', (event) => {
|
|
138
162
|
const name = event.toolName;
|
|
139
163
|
const refuse = (result) => { options.onRefuse?.(event, result.reason); return result; };
|
|
140
164
|
state.steps += 1;
|
|
141
165
|
state.lastTool = name;
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
// A handoff is callable only when checkpoint_write is active; after the slice ended it is the only one.
|
|
166
|
+
const handoff = HANDOFF_TOOLS.has(name);
|
|
167
|
+
// After the slice ended, a handoff call is the only one that runs.
|
|
145
168
|
if (state.endedBy) {
|
|
146
|
-
const reason =
|
|
147
|
-
? 'This reply is over. Its checkpoint is recorded; nothing else runs.'
|
|
148
|
-
: 'This reply is over. The Harness will record an evidence-derived checkpoint; nothing else runs.';
|
|
169
|
+
const reason = 'This reply is over. The Harness will record an evidence-derived checkpoint; nothing else runs.';
|
|
149
170
|
return handoff ? undefined : refuse({ block: true, terminate: true, reason });
|
|
150
171
|
}
|
|
151
172
|
if (state.compactions >= 3) {
|
|
@@ -158,16 +179,64 @@ export function workSliceGuard(options) {
|
|
|
158
179
|
}
|
|
159
180
|
const limit = sliceLimitReached(policy, state);
|
|
160
181
|
if (limit) {
|
|
161
|
-
// The slice ends here; the model gets one wrap-up request to
|
|
182
|
+
// The slice ends here; the model gets one wrap-up request to report where it is. A handoff
|
|
162
183
|
// call is never refused, including when it is the very first call past the budget.
|
|
163
184
|
state.endedBy = 'budget';
|
|
164
185
|
state.endedByLimit = limit;
|
|
165
186
|
state.refusedAt ??= state.steps;
|
|
166
|
-
return handoff ? undefined : refuse({ block: true, reason: handoffRequest(limit, state
|
|
187
|
+
return handoff ? undefined : refuse({ block: true, reason: handoffRequest(limit, state) });
|
|
167
188
|
}
|
|
168
189
|
});
|
|
169
190
|
};
|
|
170
191
|
}
|
|
192
|
+
export const SLICE_CONTINUE_CUSTOM_TYPE = 'xingular-slice-continue';
|
|
193
|
+
/** Successful tools that change the workspace count as progress. */
|
|
194
|
+
const PROGRESS_TOOLS = new Set(['bash', 'edit', 'write']);
|
|
195
|
+
/**
|
|
196
|
+
* Continues a reply in place when its slice ended at the budget (PROP-0041, ADR-0070): the same
|
|
197
|
+
* run and the same cached prompt, with fresh counters, up to `maxSlices`. Never after a fruitless,
|
|
198
|
+
* blocked, failed, aborted or plan slice, or when every todo item is done. A person's message
|
|
199
|
+
* starts again at slice 1.
|
|
200
|
+
*/
|
|
201
|
+
export function sliceContinuation(options) {
|
|
202
|
+
const { policy, state } = options;
|
|
203
|
+
return (pi) => {
|
|
204
|
+
pi.on('before_agent_start', (event) => {
|
|
205
|
+
if (event.messageType !== undefined && event.messageType !== 'user')
|
|
206
|
+
return;
|
|
207
|
+
state.slice = 1;
|
|
208
|
+
});
|
|
209
|
+
pi.on('tool_result', (event) => {
|
|
210
|
+
if (!event.isError && PROGRESS_TOOLS.has(event.toolName))
|
|
211
|
+
state.progressed = true;
|
|
212
|
+
});
|
|
213
|
+
pi.on('agent_before_settle', (event, ctx) => {
|
|
214
|
+
if (event.outcome !== 'completed' || event.continue || ctx?.signal?.aborted)
|
|
215
|
+
return;
|
|
216
|
+
if (!policy.enabled || !policy.continue || state.endedBy !== 'budget')
|
|
217
|
+
return;
|
|
218
|
+
if (options.active && !options.active())
|
|
219
|
+
return;
|
|
220
|
+
if (options.failed?.() || options.blocked?.() || !state.progressed)
|
|
221
|
+
return;
|
|
222
|
+
if (state.slice >= policy.maxSlices)
|
|
223
|
+
return;
|
|
224
|
+
const todo = options.todo?.() ?? [];
|
|
225
|
+
if (!options.goalActive?.() && todo.length && todo.every((item) => item.status === 'completed'))
|
|
226
|
+
return;
|
|
227
|
+
const ended = { slice: state.slice, steps: state.steps, limit: state.endedByLimit };
|
|
228
|
+
options.onContinue?.(ended);
|
|
229
|
+
beginNextSlice(state);
|
|
230
|
+
const next = options.next?.().trim();
|
|
231
|
+
const content = clip(`Work slice ${ended.slice} reached its limit (${ended.limit ?? 'budget'}). Slice ${state.slice} of up to ${policy.maxSlices} starts now with a fresh budget. ` +
|
|
232
|
+
`Continue the task from ${next ? `the next step: ${next}` : 'the next step you reported'}. Do not redo finished work.`, 900);
|
|
233
|
+
return {
|
|
234
|
+
entries: [...event.entries, { type: 'custom_message', customType: SLICE_CONTINUE_CUSTOM_TYPE, content, display: true }],
|
|
235
|
+
continue: true
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
}
|
|
171
240
|
/**
|
|
172
241
|
* Which slice of its session a run is, from 1. Counts the recorded continuation chain, so it
|
|
173
242
|
* survives a restart and needs no extra field on the run record.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RunEvent, type RunRecord } from "../sessions/contracts.js";
|
|
2
|
-
import type { ActiveRun, ApprovalDecision, QuestionAnswer } from "../sessions/registry.js";
|
|
2
|
+
import type { ActiveRun, ApprovalDecision, InterruptStop, QuestionAnswer } from "../sessions/registry.js";
|
|
3
3
|
import { type TerminalSubagentActivity, type TerminalSubagentProfile, type TerminalSubagentRun } from "./subagentMonitor.js";
|
|
4
4
|
type MonitorRuntime = Pick<TerminalSubagentRun, 'accountLabel' | 'model' | 'effort'>;
|
|
5
5
|
/** A saved run to show in the monitor after the conversation is reopened. */
|
|
@@ -48,7 +48,7 @@ export interface ControllerHooks {
|
|
|
48
48
|
*/
|
|
49
49
|
hydrate?(owners: string[]): void;
|
|
50
50
|
telemetry(event: string): void;
|
|
51
|
-
interrupt?(runId: string): Promise<void>;
|
|
51
|
+
interrupt?(runId: string, stop?: InterruptStop): Promise<void>;
|
|
52
52
|
resolveApproval?(runId: string, requestId: string, decision: ApprovalDecision): void;
|
|
53
53
|
answerQuestion?(runId: string, requestId: string, answer: QuestionAnswer): void;
|
|
54
54
|
answerQuestions?(runId: string, batchId: string, answers: Array<{
|
|
@@ -8,6 +8,8 @@ import { runStatusSchema } from "../sessions/contracts.js";
|
|
|
8
8
|
import { briefTitle } from "../sessions/subagents.js";
|
|
9
9
|
import { MAIN_PROFILE, SubagentMonitor } from "./subagentMonitor.js";
|
|
10
10
|
import { replyStalled } from "./stallWatch.js";
|
|
11
|
+
/** Stop codes the native extension may report for a reply it settled itself. */
|
|
12
|
+
const SETTLED_CODES = new Set(['slice_budget', 'compaction_thrash', 'needs_person', 'unfinished']);
|
|
11
13
|
function monitorRun(record, relation, profile, runtime) {
|
|
12
14
|
return {
|
|
13
15
|
id: record.id,
|
|
@@ -274,8 +276,13 @@ export class TerminalController {
|
|
|
274
276
|
return { requests: [...this.pending.values()] };
|
|
275
277
|
if (call === 'terminal.pending.wait')
|
|
276
278
|
return this.waitPending(data.revision);
|
|
277
|
-
if (call === 'terminal.interrupt')
|
|
278
|
-
|
|
279
|
+
if (call === 'terminal.interrupt') {
|
|
280
|
+
const run = this.activeRun;
|
|
281
|
+
if (!run || !this.hooks.interrupt)
|
|
282
|
+
return {};
|
|
283
|
+
// Pi's abort reports through here as a person's Esc does; one the stall watch asked for is a stall.
|
|
284
|
+
return this.hooks.interrupt(run.runId, this.stallRequested === run.runId ? { reason: 'stalled', code: 'stalled' } : undefined);
|
|
285
|
+
}
|
|
279
286
|
if (call === 'terminal.policy') {
|
|
280
287
|
const run = this.activeRun;
|
|
281
288
|
if (!run || typeof data.tool !== 'string' || !data.tool)
|
|
@@ -436,11 +443,12 @@ export class TerminalController {
|
|
|
436
443
|
}
|
|
437
444
|
if (event === 'settled') {
|
|
438
445
|
// A reply that stopped at its work-slice boundary is interrupted, not completed: the record
|
|
439
|
-
// says `slice_complete` (or `stalled`) so a host can continue it (PROP-0041).
|
|
446
|
+
// says `slice_complete` (or `stalled`) so a host can continue it (PROP-0041). So is one that
|
|
447
|
+
// ended with open todo items: `needs_person` or `unfinished` (ADR-0069).
|
|
440
448
|
const stalled = this.stallRequested !== null && this.stallRequested === this.run?.runId;
|
|
441
449
|
const reason = data.error ? undefined : stalled ? 'stalled' : typeof data.reason === 'string' ? data.reason : undefined;
|
|
442
450
|
const outcome = data.error ? 'failed' : data.aborted || reason ? 'interrupted' : 'completed';
|
|
443
|
-
const code = data.error ? data.errorCode === 'provider_error' ? 'provider_error' : undefined : stalled ? 'stalled' :
|
|
451
|
+
const code = data.error ? data.errorCode === 'provider_error' ? 'provider_error' : undefined : stalled ? 'stalled' : SETTLED_CODES.has(data.code) ? data.code : data.aborted ? 'user_abort' : undefined;
|
|
444
452
|
this.finish(outcome, data.error ? String(data.error) : undefined, reason, code);
|
|
445
453
|
return {};
|
|
446
454
|
}
|
|
@@ -496,6 +504,8 @@ export class TerminalController {
|
|
|
496
504
|
run.emit('todo.updated', { items: data.items });
|
|
497
505
|
else if (event === 'checkpoint')
|
|
498
506
|
run.emit('checkpoint', data);
|
|
507
|
+
else if (event === 'notice')
|
|
508
|
+
run.emit('extension.notice', data);
|
|
499
509
|
else if (event === 'file')
|
|
500
510
|
run.emit('file.changed', data);
|
|
501
511
|
else if (event === 'delta')
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CredentialStore } from '@earendil-works/pi-ai';
|
|
2
|
+
type Call = <T>(call: string, params?: Record<string, unknown>, timeoutMs?: number, signal?: AbortSignal) => Promise<T>;
|
|
3
|
+
export declare function terminalCredentialStore(call?: Call): CredentialStore;
|
|
4
|
+
export {};
|