@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,23 @@
|
|
|
1
|
+
# Xingular Harness 1.5.0-unstable
|
|
2
|
+
|
|
3
|
+
Unstable preview of the current Harness work. Install with `npm install @xingular/harness@unstable`.
|
|
4
|
+
|
|
5
|
+
- **No todo gate.** The stop gate, its three reminders and the `unfinished` status that open todo
|
|
6
|
+
items produced are removed. A reply the model ends settles `completed`; the goal loop and the
|
|
7
|
+
work-slice guard keep their stop codes and checkpoints. `XINGULAR_STOP_GATE` is retired.
|
|
8
|
+
- **Codex's final-answer guidance.** The reporting section adopts Codex's `gpt-6.1-sol` wording:
|
|
9
|
+
self-contained final, outcome first, what changed and why, how it was tested, risks and limits,
|
|
10
|
+
evidence summarized, tables for comparisons, length by complexity, prose before lists. It falls
|
|
11
|
+
from 9.2k to 3.7k characters.
|
|
12
|
+
- **Personality and model identity.** A person's own session reads Codex's personality paragraph,
|
|
13
|
+
and the identity names the model the session runs when the host knows it. The unclear-scope rule
|
|
14
|
+
uses Codex's wording with `ask_user` for the follow-up.
|
|
15
|
+
- **Work rules and credentials.** The tool guidance gains Codex's `rg`-first search, shell quoting,
|
|
16
|
+
command-separator, variable-name and test-restraint rules, plus the skill and plugin principles.
|
|
17
|
+
A login that blocks the work is answered with the exact command.
|
|
18
|
+
|
|
19
|
+
Test suites were not run for this candidate, at the request to bypass tests.
|
|
20
|
+
|
|
21
|
+
Prepared from commit a9c25b3. Not published in the session that prepared it: npm
|
|
22
|
+
authentication was unavailable (the registry answered 401). Follow `docs/releases/README.md` before
|
|
23
|
+
publishing.
|
|
@@ -9,6 +9,13 @@ related:
|
|
|
9
9
|
---
|
|
10
10
|
# How other harnesses control the end of a turn
|
|
11
11
|
|
|
12
|
+
> **Update (2026-11-01, [ADR-0075](../decisions/ADR-0075-no-todo-gate.md)):** the todo gate is
|
|
13
|
+
> removed. A peer check found that omp ships the same three-reminder gate on by default
|
|
14
|
+
> (`todo.remindersMax` 3), while Claude Code only reminds the model to use TodoWrite
|
|
15
|
+
> (`ccVersion` 2.1.139), Codex's `update_plan` is opt-in (0.152) and Kimi CLI has no nudge. A reply
|
|
16
|
+
> the model ends now settles `completed`; the goal loop and the work-slice guard keep their stop
|
|
17
|
+
> codes and checkpoints.
|
|
18
|
+
|
|
12
19
|
> **Update (2026-11-01, [ADR-0072](../decisions/ADR-0072-codex-prompts-and-goal.md)):** OpenAI Codex is open source. It has no completion check on ordinary turns and keeps its model-called `update_goal` behind an opt-in `/goal`. The harness followed: ordinary replies rely on Codex's prompt wording and the todo gate, and the judge now checks a `/goal` completion claim only.
|
|
13
20
|
|
|
14
21
|
> **Update (2026-10-31, [ADR-0071](../decisions/ADR-0071-completion-check.md)):** the model-judged evaluator rejected above was adopted after all, as an automatic completion check on the run's own model, because a live A/B showed a closed todo list hiding unfinished work and `finish(needs_decision)` used as an early exit; `finish` is removed.
|
|
@@ -32,9 +32,9 @@ for every session and every application.
|
|
|
32
32
|
| Claude Code | Official docs, [Output styles](https://code.claude.com/docs/en/output-styles), checked 2026-11-01 | Default and Concise styles: the first sentence states what happened; no preamble, narration or closing recap; a simple question is answered in one to three sentences; error reports, failing test output, security warnings and destructive-action confirmations stay complete. Output styles are session-level, not a fixed report template. |
|
|
33
33
|
| Claude Code | Extracted prompt, [Away summary generation](https://raw.githubusercontent.com/Piebald-AI/claude-code-system-prompts/main/system-prompts/agent-prompt-away-summary-generation.md) (third-party extraction, `ccVersion` 2.1.173) | Recap in under 40 words, one or two plain sentences, no markdown: the overall goal, the current task, then the one next action; skip root-cause narrative and secondary to-dos. |
|
|
34
34
|
| Claude Code | Extracted prompt, [Subagent reporting notes](https://raw.githubusercontent.com/Piebald-AI/claude-code-system-prompts/main/system-prompts/agent-prompt-subagent-reporting-notes-lean-rewrite.md) (third-party extraction, `ccVersion` 2.1.301) | The final message is the report; absolute paths; code snippets only when the exact text is load-bearing; do not recap code that was read. |
|
|
35
|
-
| Codex | Repository listing, [openai/codex `codex-rs/core`](https://github.com/openai/codex/tree/main/codex-rs/core) (checked 2026-11-01) | Per-model prompt files of 6.6k–24k characters (`gpt5codexprompt.md` 6,647; `gpt-5.2-codexprompt.md` 7,589; `gpt51prompt.md` 24,204; `gpt52prompt.md` 21,652).
|
|
35
|
+
| Codex | Repository listing, [openai/codex `codex-rs/core`](https://github.com/openai/codex/tree/main/codex-rs/core) (checked 2026-11-01) | Per-model prompt files of 6.6k–24k characters (`gpt5codexprompt.md` 6,647; `gpt-5.2-codexprompt.md` 7,589; `gpt51prompt.md` 24,204; `gpt52prompt.md` 21,652). Its final-answer wording is measured below from `models.json`. |
|
|
36
36
|
| Kimi CLI | [external/kimi-cli/src/kimi_cli/agents/default/system.md](../../external/kimi-cli/src/kimi_cli/agents/default/system.md) | No final-report format. The prompt says to inform the user that the task completed or failed, or to ask for more information; the shape is left to the model. |
|
|
37
|
-
| This harness | [src/prompt/reporting.ts](../../src/prompt/reporting.ts), [04-prompt.md](../architecture/04-prompt.md) | ADR-
|
|
37
|
+
| This harness | [src/prompt/reporting.ts](../../src/prompt/reporting.ts), [04-prompt.md](../architecture/04-prompt.md) | ADR-0076: ~3.7k characters. Codex's final-answer and technical-communication text: self-contained, outcome first, change/why/tested/risks, evidence summarized, tables, length by complexity, two examples. |
|
|
38
38
|
|
|
39
39
|
### What the peer evidence supports
|
|
40
40
|
|
|
@@ -49,10 +49,46 @@ for every session and every application.
|
|
|
49
49
|
- **No single template.** Kimi has no shape; Claude Code changes shape by output style; Codex's
|
|
50
50
|
per-model files differ. A fixed heading list is this harness's own invention, not a peer norm.
|
|
51
51
|
|
|
52
|
+
## Codex's prompt measured
|
|
53
|
+
|
|
54
|
+
Checked 2026-11-01 against `openai/codex` at `126a0ee` (2026-10-26). The prompts now live in
|
|
55
|
+
`codex-rs/models-manager/models.json` as `model_messages.instructions_template`; the `gpt-6.1-sol`
|
|
56
|
+
template is 21,769 characters, 4,212 tokens (o200k). `## Final answer` with its
|
|
57
|
+
`### Formatting rules` and `### Visualizations` subsections is 453 tokens, and
|
|
58
|
+
`## Intermediate commentary` adds 216: Codex spends 669 tokens (16%) on the text of its replies.
|
|
59
|
+
Its tone rules sit outside that block, in `## Writing style` (442) and
|
|
60
|
+
`## Technical communication` (190).
|
|
61
|
+
|
|
62
|
+
| Prompt | Characters | Tokens (o200k) | Share of its own prompt |
|
|
63
|
+
|---|---:|---:|---:|
|
|
64
|
+
| Codex `## Final answer` (with formatting and visualizations) | 2,318 | 453 | 10.8% |
|
|
65
|
+
| Codex final answer plus intermediate commentary | 3,413 | 669 | 15.9% |
|
|
66
|
+
| This harness's `reporting` section, ADR-0073 (superseded) | 15,808 | 3,736 | ~54% of a ~6.9k-token prefix |
|
|
67
|
+
| This harness's `reporting` section, ADR-0074 (superseded) | 8,921 | 2,108 | ~40% of a ~5.3k-token prefix |
|
|
68
|
+
| This harness's `reporting` section, ADR-0076 | 3,697 | 792 | ~20% of a ~4.0k-token prefix |
|
|
69
|
+
| The three worked examples inside it | 1,777 | 425 | 20.2% of the section |
|
|
70
|
+
|
|
71
|
+
ADR-0073's four required blocks did not survive use. Replies listed the agent's own commands —
|
|
72
|
+
`npm run typecheck — passed`, `npm test — 212 test files passed` — under a heading meant for the
|
|
73
|
+
person's testing, ended with "Nothing needed next", and reused "What changed / How to check / Next
|
|
74
|
+
step" on unrelated work. ADR-0074 replaced the blocks with content questions, made the by-hand
|
|
75
|
+
section optional and human-only, banned the filler ending, and cut the text to 8,921 characters
|
|
76
|
+
(2,108 tokens). The Codex numbers above are the baseline that made a large section defensible; the
|
|
77
|
+
failure was in what the blocks asked for, not in their size.
|
|
78
|
+
|
|
79
|
+
ADR-0076 then adopted Codex's wording for the report. The section is 3,697 characters, 792 tokens:
|
|
80
|
+
1.7 times Codex's final-answer block and 19% of Codex's 4,212-token prompt. It is no longer the
|
|
81
|
+
largest fixed section; the ranking and the remaining budget are recorded in
|
|
82
|
+
[04-prompt.md](../architecture/04-prompt.md). The section keeps one advantage over Codex's: a login
|
|
83
|
+
that blocks the work is answered with the exact command, in `autonomy` and in the blocked-publish
|
|
84
|
+
example.
|
|
85
|
+
|
|
86
|
+
The 6.6k–24k character sizes in the peer table above describe the per-model `*prompt.md` files that
|
|
87
|
+
earlier listings named; the prompt source has since moved into `models.json`, so those sizes are not
|
|
88
|
+
the text measured here.
|
|
89
|
+
|
|
52
90
|
## What could not be verified
|
|
53
91
|
|
|
54
|
-
- Codex's exact final-answer rules: the raw prompt files in `openai/codex` returned 404 through the
|
|
55
|
-
fetch path; only the file names and sizes are verified.
|
|
56
92
|
- Claude Code's prompt is a third-party extraction from the compiled package, not an Anthropic
|
|
57
93
|
publication. The official output-styles page was used for the Default and Concise behavior; the
|
|
58
94
|
extracted prompts are cited as a secondary source and versioned by `ccVersion`.
|
|
@@ -62,19 +98,21 @@ for every session and every application.
|
|
|
62
98
|
## Design implications
|
|
63
99
|
|
|
64
100
|
1. Keep the section foundation-owned and stable; no variant or application should have to repeat it.
|
|
65
|
-
2. Define
|
|
66
|
-
3.
|
|
67
|
-
|
|
68
|
-
4. Spend the prompt budget on rules, anti-
|
|
69
|
-
with real requests ([INV-PRM-8](../architecture/04-prompt.md)).
|
|
101
|
+
2. Define content questions, not fixed blocks or headings; the four block names are gone.
|
|
102
|
+
3. Treat the agent's runs as one line of evidence, not steps for the person; a by-hand check covers
|
|
103
|
+
only what a person alone can do, and is absent when nothing needs one.
|
|
104
|
+
4. Spend the prompt budget on rules, an anti-pattern table and three worked examples; measure the
|
|
105
|
+
cache cost with real requests ([INV-PRM-8](../architecture/04-prompt.md)).
|
|
70
106
|
5. Keep the reply short: a question stays one to three sentences, and the change report leads with
|
|
71
|
-
the outcome and the
|
|
107
|
+
the outcome and the person's check.
|
|
72
108
|
|
|
73
109
|
## Sources
|
|
74
110
|
|
|
75
111
|
- [Claude Code: Output styles](https://code.claude.com/docs/en/output-styles), fetched 2026-11-01.
|
|
76
112
|
- [Piebald-AI/claude-code-system-prompts](https://github.com/Piebald-AI/claude-code-system-prompts),
|
|
77
113
|
`README.md` and the two extracted prompt files linked above, fetched 2026-11-01.
|
|
114
|
+
- [openai/codex `codex-rs/models-manager/models.json`](https://github.com/openai/codex/blob/126a0ee92ed2c5e73cd8a8f7e37563e8527552d7/codex-rs/models-manager/models.json),
|
|
115
|
+
`model_messages.instructions_template` of `gpt-6.1-sol`, fetched 2026-11-01.
|
|
78
116
|
- [openai/codex `codex-rs/core`](https://github.com/openai/codex/tree/main/codex-rs/core), directory
|
|
79
117
|
listing fetched 2026-11-01.
|
|
80
118
|
- [external/kimi-cli/src/kimi_cli/agents/default/system.md](../../external/kimi-cli/src/kimi_cli/agents/default/system.md),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xingular/harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0-unstable",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Xingular Harness — a Pi agent harness built on recursive harnessing (Harness²): versioned eight-component variants, refinement from evidence, multi-account sessions, a compiled prompt, project notes, tools, and MCP",
|
|
6
6
|
"repository": {
|
|
@@ -1,78 +0,0 @@
|
|
|
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;
|
|
@@ -1,143 +0,0 @@
|
|
|
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
|
-
}
|