@tt-a1i/openpi 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -20
- package/SETUP.md +10 -4
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/bin/openpi.js +25 -15
- package/extensions/ai-providers/LICENSE.upstream +23 -0
- package/extensions/ai-providers/README.md +65 -0
- package/extensions/ai-providers/antigravity/credentials.ts +52 -0
- package/extensions/ai-providers/antigravity/discovery.ts +130 -0
- package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
- package/extensions/ai-providers/antigravity/models.ts +84 -0
- package/extensions/ai-providers/antigravity/oauth.ts +700 -0
- package/extensions/ai-providers/antigravity/provider.ts +1116 -0
- package/extensions/ai-providers/antigravity/routing.ts +340 -0
- package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
- package/extensions/ai-providers/cursor/constants.ts +5 -0
- package/extensions/ai-providers/cursor/credentials.ts +14 -0
- package/extensions/ai-providers/cursor/discovery.ts +291 -0
- package/extensions/ai-providers/cursor/input-images.ts +105 -0
- package/extensions/ai-providers/cursor/models.ts +45 -0
- package/extensions/ai-providers/cursor/oauth.ts +263 -0
- package/extensions/ai-providers/cursor/proto.ts +1271 -0
- package/extensions/ai-providers/cursor/protobuf.ts +1181 -0
- package/extensions/ai-providers/cursor/provider.ts +1431 -0
- package/extensions/ai-providers/cursor/proxy.ts +213 -0
- package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
- package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
- package/extensions/ai-providers/index.ts +86 -0
- package/extensions/ai-providers/oauth-adapter.ts +81 -0
- package/extensions/ai-providers/usage.ts +10 -0
- package/extensions/background-terminals/index.ts +8 -1
- package/extensions/background-terminals/src/manager.ts +3 -5
- package/extensions/background-terminals/src/result-delivery.ts +43 -23
- package/extensions/cron/index.ts +68 -27
- package/extensions/cron/schedule.ts +5 -1
- package/extensions/model-info/cache-diagnostics.ts +220 -0
- package/extensions/model-info/index.ts +45 -1
- package/extensions/plan-mode/index.ts +75 -4
- package/extensions/setup/index.ts +15 -3
- package/extensions/shared/child-session.ts +39 -5
- package/extensions/shared/completion-inbox.ts +193 -0
- package/extensions/shared/setup-config.ts +10 -1
- package/extensions/shared/structured-output.ts +154 -0
- package/extensions/subagents/index.ts +64 -7
- package/extensions/subagents/src/agent-types.ts +5 -17
- package/extensions/subagents/src/backends/pi.ts +130 -48
- package/extensions/subagents/src/backends/tool-preview.ts +29 -0
- package/extensions/subagents/src/domain.ts +16 -1
- package/extensions/subagents/src/manager.ts +7 -71
- package/extensions/subagents/src/prompt.ts +19 -5
- package/extensions/subagents/src/result-artifact.ts +32 -0
- package/extensions/subagents/src/result-delivery.ts +33 -14
- package/extensions/subagents/src/runtime.ts +10 -3
- package/extensions/ui-customization/footer.ts +16 -5
- package/extensions/user-input-fold/index.ts +42 -6
- package/extensions/web/index.ts +25 -2
- package/extensions/workflows/acceptance.ts +43 -19
- package/extensions/workflows/completion-projection.ts +3 -1
- package/extensions/workflows/dashboard.ts +147 -21
- package/extensions/workflows/index.ts +75 -20
- package/extensions/workflows/model.ts +5 -1
- package/extensions/workflows/progress-projection.ts +7 -1
- package/extensions/workflows/prompt.ts +4 -10
- package/extensions/workflows/result-delivery.ts +96 -22
- package/extensions/workflows/retention.ts +6 -0
- package/extensions/workflows/runner.ts +11 -233
- package/extensions/workflows/sandbox.ts +4 -0
- package/package.json +7 -7
- package/skills/subagents/REFERENCE.md +9 -9
- package/skills/subagents/SKILL.md +2 -1
- package/skills/workflows/REFERENCE.md +5 -3
- package/skills/workflows/SKILL.md +1 -1
- package/web/adapter/pi-adapter.ts +3 -0
- package/web/host/pi-coding-agent-entry.ts +162 -0
- package/web/host/web-host.ts +330 -50
- package/web/protocol/types.ts +5 -0
- package/web/runtime/pi-runtime.ts +240 -25
- package/web/runtime/types.ts +32 -1
- package/web/ui/app.js +343 -41
- package/web/ui/index.html +3 -0
- package/web/ui/styles.css +119 -37
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
|
|
2
|
+
import {
|
|
3
|
+
type CompletionOwner,
|
|
4
|
+
createCompletionInbox,
|
|
5
|
+
} from "../../shared/completion-inbox.ts";
|
|
2
6
|
|
|
3
7
|
export interface SubagentResultDeliveryOptions<T> {
|
|
4
8
|
/** True only when the parent has no run or queued continuation in flight. */
|
|
5
9
|
readonly isIdle: () => boolean;
|
|
6
10
|
/** Deliver one drained batch and wake the parent. */
|
|
7
11
|
readonly deliver: (results: readonly T[]) => void;
|
|
12
|
+
/** Current Pi Session transcript owner. */
|
|
13
|
+
readonly owner?: () => CompletionOwner | undefined;
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
/**
|
|
@@ -22,50 +28,63 @@ export interface SubagentResultDeliveryOptions<T> {
|
|
|
22
28
|
* The parent boundary wakes even if an earlier extension handler has already
|
|
23
29
|
* started another turn: Pi queues the follow-up into that active run.
|
|
24
30
|
*
|
|
25
|
-
* The
|
|
26
|
-
* is delivered, and whichever path
|
|
31
|
+
* The shared inbox is the one-shot gate: `subagent_wait` may consume a result
|
|
32
|
+
* before it is delivered, and whichever path claims first prevents duplicate
|
|
33
|
+
* delivery.
|
|
27
34
|
*/
|
|
28
35
|
export function createSubagentResultDelivery<T extends { id: string }>(
|
|
29
36
|
options: SubagentResultDeliveryOptions<T>,
|
|
30
37
|
) {
|
|
31
|
-
const
|
|
38
|
+
const inbox = createCompletionInbox<T>();
|
|
39
|
+
const owner = options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
|
|
32
40
|
|
|
33
41
|
const flush = () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
42
|
+
const envelopes = inbox.claim(owner());
|
|
43
|
+
if (envelopes.length === 0) return;
|
|
44
|
+
const results = envelopes.map((envelope) => envelope.payload);
|
|
37
45
|
try {
|
|
38
46
|
options.deliver(results);
|
|
47
|
+
inbox.acknowledge(envelopes.map((envelope) => envelope.deliveryId));
|
|
39
48
|
} catch (error) {
|
|
40
49
|
// A synchronous session teardown may reject append/send. Preserve the
|
|
41
50
|
// original batch ahead of anything deferred re-entrantly while delivery
|
|
42
51
|
// ran, so a later boundary can retry without loss or reordering.
|
|
43
|
-
|
|
44
|
-
pending.clear();
|
|
45
|
-
for (const result of results) pending.set(result.id, result);
|
|
46
|
-
for (const result of current) pending.set(result.id, result);
|
|
52
|
+
inbox.retry(envelopes, owner());
|
|
47
53
|
throw error;
|
|
48
54
|
}
|
|
49
55
|
};
|
|
50
56
|
|
|
51
57
|
const queue = {
|
|
52
58
|
defer(result: T) {
|
|
53
|
-
|
|
59
|
+
const currentOwner = owner();
|
|
60
|
+
inbox.defer(
|
|
61
|
+
{
|
|
62
|
+
deliveryId: `subagent:${result.id}`,
|
|
63
|
+
owner: currentOwner ?? { sessionId: "unowned", epoch: 0 },
|
|
64
|
+
producer: "subagent",
|
|
65
|
+
producerId: result.id,
|
|
66
|
+
terminalRef: { kind: "subagent-snapshot", id: result.id },
|
|
67
|
+
wake: "follow-up",
|
|
68
|
+
payload: result,
|
|
69
|
+
},
|
|
70
|
+
currentOwner,
|
|
71
|
+
);
|
|
54
72
|
if (options.isIdle()) flush();
|
|
55
73
|
},
|
|
56
74
|
consume(ids: Iterable<string>) {
|
|
57
|
-
|
|
75
|
+
inbox.consume("subagent", ids);
|
|
58
76
|
},
|
|
59
77
|
/** Flush at the authoritative parent boundary. */
|
|
60
78
|
parentSettled() {
|
|
61
79
|
flush();
|
|
62
80
|
},
|
|
63
81
|
clear() {
|
|
64
|
-
|
|
82
|
+
inbox.clear();
|
|
65
83
|
},
|
|
66
84
|
size() {
|
|
67
|
-
return
|
|
85
|
+
return inbox.size();
|
|
68
86
|
},
|
|
87
|
+
inspectDeadLetters: inbox.inspectDeadLetters,
|
|
69
88
|
};
|
|
70
89
|
return queue satisfies ConsumableResultDeliveryQueue<T>;
|
|
71
90
|
}
|
|
@@ -45,6 +45,9 @@ export function createSubagentRuntime(config: SubagentManagerConfig = {}) {
|
|
|
45
45
|
|
|
46
46
|
export type SubagentRuntime = ReturnType<typeof createSubagentRuntime>;
|
|
47
47
|
|
|
48
|
+
/** Canonical interruption, distinct from a known startup failure. */
|
|
49
|
+
export class SubagentToolInterruptedError extends Error {}
|
|
50
|
+
|
|
48
51
|
/**
|
|
49
52
|
* Run an effect from an async tool handler. Typed failures and defects are
|
|
50
53
|
* converted to thrown Errors (what pi's tool contract expects); interruption
|
|
@@ -60,9 +63,13 @@ export async function runTool<A, E>(
|
|
|
60
63
|
options.signal ? { signal: options.signal } : undefined,
|
|
61
64
|
);
|
|
62
65
|
if (Exit.isSuccess(exit)) return exit.value;
|
|
63
|
-
if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
64
|
-
throw new Error(options.interruptMessage ?? "Operation was aborted.");
|
|
65
|
-
}
|
|
66
66
|
const [first] = Cause.prettyErrors(exit.cause);
|
|
67
|
+
if (Cause.hasInterrupts(exit.cause)) {
|
|
68
|
+
const interrupted = options.interruptMessage ?? "Operation was aborted.";
|
|
69
|
+
const detail = Cause.hasInterruptsOnly(exit.cause)
|
|
70
|
+
? ""
|
|
71
|
+
: ` ${first?.message ?? Cause.pretty(exit.cause)}`;
|
|
72
|
+
throw new SubagentToolInterruptedError(`${interrupted}${detail}`);
|
|
73
|
+
}
|
|
67
74
|
throw new Error(first?.message ?? Cause.pretty(exit.cause));
|
|
68
75
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import {
|
|
2
|
+
import { posix, win32 } from "node:path";
|
|
3
3
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import {
|
|
5
5
|
getCapabilities,
|
|
@@ -117,10 +117,21 @@ export function formatTokens(tokens: number) {
|
|
|
117
117
|
return `${(tokens / 1_000_000).toFixed(1)}m`;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
export function formatDirectory(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
export function formatDirectory(
|
|
121
|
+
cwd: string,
|
|
122
|
+
home = homedir(),
|
|
123
|
+
pathModule = process.platform === "win32" ? win32 : posix,
|
|
124
|
+
) {
|
|
125
|
+
const relativePath = pathModule.relative(home, cwd);
|
|
126
|
+
const outsideHome =
|
|
127
|
+
relativePath === ".." ||
|
|
128
|
+
relativePath.startsWith(`..${pathModule.sep}`) ||
|
|
129
|
+
pathModule.isAbsolute(relativePath);
|
|
130
|
+
const display = outsideHome
|
|
131
|
+
? cwd
|
|
132
|
+
: relativePath
|
|
133
|
+
? `~/${relativePath.replaceAll(pathModule.sep, "/")}`
|
|
134
|
+
: "~";
|
|
124
135
|
return sanitizeTerminalLabel(display);
|
|
125
136
|
}
|
|
126
137
|
|
|
@@ -40,8 +40,35 @@ type Segment =
|
|
|
40
40
|
| { kind: "prose"; lines: string[] }
|
|
41
41
|
| { kind: "code"; open: string; content: string[]; close: string };
|
|
42
42
|
|
|
43
|
-
const FENCE_OPEN = /^ {0,3}`{3,}/;
|
|
44
|
-
|
|
43
|
+
const FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse an opening code fence (CommonMark §4.5): which character it uses and
|
|
47
|
+
* how long it is. A backtick fence's info string may not contain backticks.
|
|
48
|
+
*/
|
|
49
|
+
function openFence(line: string) {
|
|
50
|
+
const match = FENCE_OPEN.exec(line);
|
|
51
|
+
if (!match) return undefined;
|
|
52
|
+
const fence = match[1];
|
|
53
|
+
if (fence[0] === "`" && line.slice(match[0].length).includes("`")) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return { char: fence[0], length: fence.length };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A closing fence must use the same character as the opening fence and be at
|
|
61
|
+
* least as long: a ``` line does not close a ```` block, and backticks never
|
|
62
|
+
* close a tilde block.
|
|
63
|
+
*/
|
|
64
|
+
function isCloseFence(line: string, open: { char: string; length: number }) {
|
|
65
|
+
const match = /^ {0,3}(`{3,}|~{3,})[ \t]*\r?$/.exec(line);
|
|
66
|
+
return (
|
|
67
|
+
match !== null &&
|
|
68
|
+
match[1][0] === open.char &&
|
|
69
|
+
match[1].length >= open.length
|
|
70
|
+
);
|
|
71
|
+
}
|
|
45
72
|
|
|
46
73
|
function countLines(markdown: string) {
|
|
47
74
|
const parts = markdown.split("\n");
|
|
@@ -60,7 +87,8 @@ function parseSegments(lines: string[]): Segment[] {
|
|
|
60
87
|
let prose: string[] = [];
|
|
61
88
|
let i = 0;
|
|
62
89
|
while (i < lines.length) {
|
|
63
|
-
|
|
90
|
+
const fence = openFence(lines[i]);
|
|
91
|
+
if (!fence) {
|
|
64
92
|
prose.push(lines[i]);
|
|
65
93
|
i += 1;
|
|
66
94
|
continue;
|
|
@@ -74,13 +102,21 @@ function parseSegments(lines: string[]): Segment[] {
|
|
|
74
102
|
let close: string | undefined;
|
|
75
103
|
let j = i + 1;
|
|
76
104
|
while (j < lines.length && close === undefined) {
|
|
77
|
-
if (
|
|
105
|
+
if (isCloseFence(lines[j], fence)) close = lines[j];
|
|
78
106
|
else content.push(lines[j]);
|
|
79
107
|
j += 1;
|
|
80
108
|
}
|
|
81
109
|
if (close === undefined) {
|
|
82
|
-
// Unterminated fence:
|
|
83
|
-
|
|
110
|
+
// Unterminated fence: the block runs to the end of the message. Keep it
|
|
111
|
+
// as a code block with a synthesized closing fence so a folded preview
|
|
112
|
+
// never leaks an unclosed fence into the TUI.
|
|
113
|
+
segments.push({
|
|
114
|
+
kind: "code",
|
|
115
|
+
open,
|
|
116
|
+
content,
|
|
117
|
+
close: fence.char.repeat(fence.length),
|
|
118
|
+
});
|
|
119
|
+
return segments;
|
|
84
120
|
}
|
|
85
121
|
segments.push({ kind: "code", open, content, close });
|
|
86
122
|
i = j;
|
package/extensions/web/index.ts
CHANGED
|
@@ -5,6 +5,11 @@ import type {
|
|
|
5
5
|
ExtensionAPI,
|
|
6
6
|
ExtensionCommandContext,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import {
|
|
9
|
+
missingPiCodingAgentDiagnostic,
|
|
10
|
+
PI_CODING_AGENT_ENTRY_ENV,
|
|
11
|
+
resolvePiCodingAgentEntry,
|
|
12
|
+
} from "../../web/host/pi-coding-agent-entry.ts";
|
|
8
13
|
|
|
9
14
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
10
15
|
|
|
@@ -26,12 +31,20 @@ interface SpawnWebOptions {
|
|
|
26
31
|
stdio: "inherit";
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
function webProcessEnvironment(
|
|
34
|
+
function webProcessEnvironment(
|
|
35
|
+
cwd: string,
|
|
36
|
+
piCodingAgentEntry: string | undefined,
|
|
37
|
+
) {
|
|
30
38
|
const environment: NodeJS.ProcessEnv = { ...process.env, PWD: cwd };
|
|
31
39
|
delete environment.OLDPWD;
|
|
32
40
|
delete environment.INIT_CWD;
|
|
33
41
|
delete environment.PI_SESSION_ID;
|
|
34
42
|
delete environment.PI_SESSION_FILE;
|
|
43
|
+
if (piCodingAgentEntry) {
|
|
44
|
+
environment[PI_CODING_AGENT_ENTRY_ENV] = piCodingAgentEntry;
|
|
45
|
+
} else {
|
|
46
|
+
delete environment[PI_CODING_AGENT_ENTRY_ENV];
|
|
47
|
+
}
|
|
35
48
|
return environment;
|
|
36
49
|
}
|
|
37
50
|
|
|
@@ -40,6 +53,7 @@ export interface WebCommandDependencies {
|
|
|
40
53
|
spawn(command: string, args: string[], options: SpawnWebOptions): WebProcess;
|
|
41
54
|
clearTerminal(): void;
|
|
42
55
|
holdParentSigint(): () => void;
|
|
56
|
+
resolvePiCodingAgentEntry(): string | undefined;
|
|
43
57
|
shutdownTimeoutMs: number;
|
|
44
58
|
}
|
|
45
59
|
|
|
@@ -65,6 +79,8 @@ const defaultDependencies: WebCommandDependencies = {
|
|
|
65
79
|
process.on("SIGINT", keepPiAlive);
|
|
66
80
|
return () => process.removeListener("SIGINT", keepPiAlive);
|
|
67
81
|
},
|
|
82
|
+
resolvePiCodingAgentEntry: () =>
|
|
83
|
+
resolvePiCodingAgentEntry({ source: "host" }),
|
|
68
84
|
shutdownTimeoutMs: DEFAULT_SHUTDOWN_TIMEOUT_MS,
|
|
69
85
|
};
|
|
70
86
|
|
|
@@ -95,6 +111,7 @@ function runWebInForeground(
|
|
|
95
111
|
dependencies: WebCommandDependencies,
|
|
96
112
|
setActive: (active: ActiveWebProcess | undefined) => void,
|
|
97
113
|
isShuttingDown: () => boolean,
|
|
114
|
+
piCodingAgentEntry: string,
|
|
98
115
|
) {
|
|
99
116
|
return ctx.ui.custom<WebExit>((tui, _theme, _keybindings, done) => {
|
|
100
117
|
let finished = false;
|
|
@@ -128,7 +145,7 @@ function runWebInForeground(
|
|
|
128
145
|
[dependencies.entrypoint, "web", "--no-workspace"],
|
|
129
146
|
{
|
|
130
147
|
cwd: childCwd,
|
|
131
|
-
env: webProcessEnvironment(childCwd),
|
|
148
|
+
env: webProcessEnvironment(childCwd, piCodingAgentEntry),
|
|
132
149
|
shell: false,
|
|
133
150
|
stdio: "inherit",
|
|
134
151
|
},
|
|
@@ -191,6 +208,11 @@ export default function web(
|
|
|
191
208
|
ctx.ui.notify("OpenPI Web Workbench is already running.", "warning");
|
|
192
209
|
return;
|
|
193
210
|
}
|
|
211
|
+
const piCodingAgentEntry = dependencies.resolvePiCodingAgentEntry();
|
|
212
|
+
if (!piCodingAgentEntry) {
|
|
213
|
+
ctx.ui.notify(missingPiCodingAgentDiagnostic(), "error");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
194
216
|
|
|
195
217
|
running = true;
|
|
196
218
|
try {
|
|
@@ -201,6 +223,7 @@ export default function web(
|
|
|
201
223
|
active = next;
|
|
202
224
|
},
|
|
203
225
|
() => shuttingDown,
|
|
226
|
+
piCodingAgentEntry,
|
|
204
227
|
);
|
|
205
228
|
if (shuttingDown) return;
|
|
206
229
|
if (result.kind === "error") {
|
|
@@ -22,6 +22,25 @@ export interface AcceptanceLedger {
|
|
|
22
22
|
readonly status: "accepted" | "rejected" | "missing" | "malformed";
|
|
23
23
|
readonly criteria: readonly AcceptanceCriterionResult[];
|
|
24
24
|
readonly errors: readonly string[];
|
|
25
|
+
/** Child-authored judgment retained only for migration; never a runtime fact. */
|
|
26
|
+
readonly authority?: "model-self-attestation";
|
|
27
|
+
readonly deprecated?: {
|
|
28
|
+
readonly since: "0.5";
|
|
29
|
+
readonly removal: "1.0";
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ACCEPTANCE_DEPRECATION_WARNING =
|
|
34
|
+
"acceptance is deprecated since OpenPI 0.5 and will be removed in 1.0; it is model self-attestation, not runtime-verified evidence, and does not determine ok";
|
|
35
|
+
|
|
36
|
+
function ledger(
|
|
37
|
+
value: Omit<AcceptanceLedger, "authority" | "deprecated">,
|
|
38
|
+
): AcceptanceLedger {
|
|
39
|
+
return {
|
|
40
|
+
...value,
|
|
41
|
+
authority: "model-self-attestation",
|
|
42
|
+
deprecated: { since: "0.5", removal: "1.0" },
|
|
43
|
+
};
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -169,6 +188,12 @@ export function isAcceptanceLedger(value: unknown): value is AcceptanceLedger {
|
|
|
169
188
|
value.status === "rejected" ||
|
|
170
189
|
value.status === "missing" ||
|
|
171
190
|
value.status === "malformed") &&
|
|
191
|
+
(value.authority === undefined ||
|
|
192
|
+
value.authority === "model-self-attestation") &&
|
|
193
|
+
(value.deprecated === undefined ||
|
|
194
|
+
(record(value.deprecated) &&
|
|
195
|
+
value.deprecated.since === "0.5" &&
|
|
196
|
+
value.deprecated.removal === "1.0")) &&
|
|
172
197
|
value.errors.every((error) => typeof error === "string") &&
|
|
173
198
|
value.criteria.every(
|
|
174
199
|
(criterion) =>
|
|
@@ -187,7 +212,7 @@ export function acceptanceInstruction(contract: AcceptanceContract) {
|
|
|
187
212
|
`- ${criterion.id}: ${criterion.description}${criterion.requiredEvidence?.length ? `; required evidence labels: ${criterion.requiredEvidence.join(", ")}` : ""}`,
|
|
188
213
|
);
|
|
189
214
|
return [
|
|
190
|
-
"
|
|
215
|
+
"Deprecated compatibility protocol: this acceptance ledger is your own model self-attestation, not runtime-verified evidence, and it does not determine execution success.",
|
|
191
216
|
"Include an `acceptance.criteria` array in structured_output with exactly these ids. Mark rejected when the criterion is not demonstrated. Evidence entries must be concise labels or concrete references; do not invent evidence.",
|
|
192
217
|
...criteria,
|
|
193
218
|
].join("\n");
|
|
@@ -198,19 +223,19 @@ export function evaluateAcceptance(
|
|
|
198
223
|
structured: unknown,
|
|
199
224
|
): AcceptanceLedger {
|
|
200
225
|
if (!record(structured) || !record(structured.acceptance)) {
|
|
201
|
-
return {
|
|
226
|
+
return ledger({
|
|
202
227
|
status: "missing",
|
|
203
228
|
criteria: [],
|
|
204
229
|
errors: ["structured result omitted acceptance"],
|
|
205
|
-
};
|
|
230
|
+
});
|
|
206
231
|
}
|
|
207
232
|
const rawCriteria = structured.acceptance.criteria;
|
|
208
233
|
if (!Array.isArray(rawCriteria)) {
|
|
209
|
-
return {
|
|
234
|
+
return ledger({
|
|
210
235
|
status: "malformed",
|
|
211
236
|
criteria: [],
|
|
212
237
|
errors: ["acceptance.criteria is not an array"],
|
|
213
|
-
};
|
|
238
|
+
});
|
|
214
239
|
}
|
|
215
240
|
const errors: string[] = [];
|
|
216
241
|
const byId = new Map<string, AcceptanceCriterionResult>();
|
|
@@ -265,14 +290,15 @@ export function evaluateAcceptance(
|
|
|
265
290
|
errors.push(`unexpected acceptance criterion "${id}"`);
|
|
266
291
|
}
|
|
267
292
|
}
|
|
268
|
-
if (errors.length)
|
|
269
|
-
|
|
293
|
+
if (errors.length)
|
|
294
|
+
return ledger({ status: "malformed", criteria: results, errors });
|
|
295
|
+
return ledger({
|
|
270
296
|
status: results.every((result) => result.status === "accepted")
|
|
271
297
|
? "accepted"
|
|
272
298
|
: "rejected",
|
|
273
299
|
criteria: results,
|
|
274
300
|
errors: [],
|
|
275
|
-
};
|
|
301
|
+
});
|
|
276
302
|
}
|
|
277
303
|
|
|
278
304
|
export function applyAcceptance(options: {
|
|
@@ -284,15 +310,13 @@ export function applyAcceptance(options: {
|
|
|
284
310
|
const ledger = options.contract
|
|
285
311
|
? evaluateAcceptance(options.contract, options.structured)
|
|
286
312
|
: undefined;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
:
|
|
295
|
-
|
|
296
|
-
: (acceptanceError ?? "Agent failed");
|
|
297
|
-
return { ok, ...(ledger ? { ledger } : {}), ...(error ? { error } : {}) };
|
|
313
|
+
const ok = options.agentOk;
|
|
314
|
+
const error = ok ? undefined : (options.agentError ?? "Agent failed");
|
|
315
|
+
return {
|
|
316
|
+
ok,
|
|
317
|
+
...(ledger
|
|
318
|
+
? { ledger, acceptanceWarning: ACCEPTANCE_DEPRECATION_WARNING }
|
|
319
|
+
: {}),
|
|
320
|
+
...(error ? { error } : {}),
|
|
321
|
+
};
|
|
298
322
|
}
|
|
@@ -230,7 +230,9 @@ function buildOperatorReport(
|
|
|
230
230
|
: "running";
|
|
231
231
|
lines.push(
|
|
232
232
|
`- [${agent.label}]${agent.phase ? ` (${agent.phase})` : ""} ${state}` +
|
|
233
|
-
(agent.acceptance
|
|
233
|
+
(agent.acceptance
|
|
234
|
+
? ` · deprecated model self-attestation ${agent.acceptance.status}`
|
|
235
|
+
: "") +
|
|
234
236
|
(agent.error ? ` — ${agent.error}` : ""),
|
|
235
237
|
);
|
|
236
238
|
}
|
|
@@ -143,18 +143,54 @@ export function readPersistedWorkflowDetails(
|
|
|
143
143
|
runId: string,
|
|
144
144
|
options: ReadPersistedRunOptions = {},
|
|
145
145
|
): WorkflowDetails | undefined {
|
|
146
|
-
|
|
146
|
+
const details = normalizeReadRecord(
|
|
147
|
+
runId,
|
|
148
|
+
readPersistedWorkflowRecord(runId),
|
|
149
|
+
);
|
|
150
|
+
if (!details) return undefined;
|
|
151
|
+
if (options.hydrateArtifacts) hydrateRunArtifacts(runId, details);
|
|
152
|
+
return details;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeReadRecord(runId: string, raw: unknown) {
|
|
156
|
+
try {
|
|
157
|
+
return normalizePersistedWorkflowDetails(runId, raw);
|
|
158
|
+
} catch {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readPersistedWorkflowRecord(runId: string) {
|
|
147
164
|
try {
|
|
148
165
|
const raw: unknown = JSON.parse(
|
|
149
166
|
fs.readFileSync(path.join(runsDir(), runId, "workflow.json"), "utf8"),
|
|
150
167
|
);
|
|
151
|
-
|
|
168
|
+
return raw && typeof raw === "object"
|
|
169
|
+
? (raw as Record<string, unknown>)
|
|
170
|
+
: undefined;
|
|
152
171
|
} catch {
|
|
153
172
|
return undefined;
|
|
154
173
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function matchesRunScope(
|
|
177
|
+
record: { startedAt?: unknown; finishedAt?: unknown; sessionId?: unknown },
|
|
178
|
+
runId: string,
|
|
179
|
+
sessionId: string,
|
|
180
|
+
referencedRunIds: ReadonlySet<string>,
|
|
181
|
+
startedSince: number,
|
|
182
|
+
fromRetention = false,
|
|
183
|
+
) {
|
|
184
|
+
const touchedAt = Math.max(
|
|
185
|
+
typeof record.startedAt === "number" ? record.startedAt : 0,
|
|
186
|
+
typeof record.finishedAt === "number" ? record.finishedAt : 0,
|
|
187
|
+
);
|
|
188
|
+
return (
|
|
189
|
+
touchedAt >= startedSince &&
|
|
190
|
+
(fromRetention ||
|
|
191
|
+
record.sessionId === sessionId ||
|
|
192
|
+
referencedRunIds.has(runId))
|
|
193
|
+
);
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
function isWorktreeCleanup(
|
|
@@ -220,6 +256,14 @@ function normalizeDelivery(value: unknown): WorkflowDetails["delivery"] {
|
|
|
220
256
|
: 0;
|
|
221
257
|
return {
|
|
222
258
|
id: sanitizeLine(record.id, 256),
|
|
259
|
+
...(typeof record.ownerSessionId === "string" && record.ownerSessionId
|
|
260
|
+
? { ownerSessionId: sanitizeLine(record.ownerSessionId, 256) }
|
|
261
|
+
: {}),
|
|
262
|
+
...(typeof record.ownerEpoch === "number" &&
|
|
263
|
+
Number.isSafeInteger(record.ownerEpoch) &&
|
|
264
|
+
record.ownerEpoch >= 0
|
|
265
|
+
? { ownerEpoch: record.ownerEpoch }
|
|
266
|
+
: {}),
|
|
223
267
|
state,
|
|
224
268
|
attempts,
|
|
225
269
|
updatedAt,
|
|
@@ -573,27 +617,41 @@ export function loadRunEntries(
|
|
|
573
617
|
retained: ReadonlyMap<string, WorkflowDetails> = new Map(),
|
|
574
618
|
): RunEntry[] {
|
|
575
619
|
const entries: RunEntry[] = [];
|
|
576
|
-
const runIds = new Set([
|
|
620
|
+
const runIds = new Set([
|
|
621
|
+
...listPersistedRunIds(),
|
|
622
|
+
...retained.keys(),
|
|
623
|
+
...active.keys(),
|
|
624
|
+
]);
|
|
577
625
|
for (const runId of runIds) {
|
|
578
626
|
const live = active.get(runId);
|
|
579
627
|
if (live) {
|
|
580
628
|
entries.push({ runId, details: live, live: true });
|
|
581
629
|
continue;
|
|
582
630
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
631
|
+
// Reject unrelated history before normalizing potentially large inline
|
|
632
|
+
// transcripts. Side artifacts belong to explicit detail navigation.
|
|
633
|
+
const raw = readPersistedWorkflowRecord(runId);
|
|
634
|
+
if (
|
|
635
|
+
raw &&
|
|
636
|
+
!matchesRunScope(raw, runId, sessionId, referencedRunIds, startedSince)
|
|
637
|
+
) {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const persisted = normalizeReadRecord(runId, raw);
|
|
586
641
|
const retainedDetails = retained.get(runId);
|
|
587
642
|
const details = persisted ?? retainedDetails;
|
|
588
643
|
if (!details) continue;
|
|
589
644
|
const fromRetention =
|
|
590
645
|
persisted === undefined && retainedDetails !== undefined;
|
|
591
|
-
const touchedAt = Math.max(details.startedAt, details.finishedAt ?? 0);
|
|
592
646
|
if (
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
647
|
+
!matchesRunScope(
|
|
648
|
+
details,
|
|
649
|
+
runId,
|
|
650
|
+
sessionId,
|
|
651
|
+
referencedRunIds,
|
|
652
|
+
startedSince,
|
|
653
|
+
fromRetention,
|
|
654
|
+
)
|
|
597
655
|
) {
|
|
598
656
|
continue;
|
|
599
657
|
}
|
|
@@ -710,6 +768,9 @@ type DetailFocus = "phases" | "agents";
|
|
|
710
768
|
export class WorkflowDashboard {
|
|
711
769
|
private view: View = "list";
|
|
712
770
|
private entries: RunEntry[] = [];
|
|
771
|
+
private historyLoaded = false;
|
|
772
|
+
private seenRetainedRunIds = new Set<string>();
|
|
773
|
+
private hydratedRunIds = new Set<string>();
|
|
713
774
|
private listIndex = 0;
|
|
714
775
|
private phaseIndex = 0;
|
|
715
776
|
private agentIndex = 0;
|
|
@@ -818,13 +879,68 @@ export class WorkflowDashboard {
|
|
|
818
879
|
|
|
819
880
|
private refresh() {
|
|
820
881
|
const selected = this.entries[this.listIndex]?.runId;
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
this.
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
882
|
+
const active = this.getActive();
|
|
883
|
+
const retained = this.getRetained();
|
|
884
|
+
if (!this.historyLoaded) {
|
|
885
|
+
this.entries = loadRunEntries(
|
|
886
|
+
active,
|
|
887
|
+
this.sessionId,
|
|
888
|
+
this.referencedRunIds,
|
|
889
|
+
this.startedSince,
|
|
890
|
+
retained,
|
|
891
|
+
);
|
|
892
|
+
this.historyLoaded = true;
|
|
893
|
+
} else {
|
|
894
|
+
// Animation ticks reuse historical projections. Only a newly settled run
|
|
895
|
+
// needs one canonical read; stable frames never scan or reread history.
|
|
896
|
+
const entries = new Map(
|
|
897
|
+
this.entries.map((entry) => [entry.runId, entry]),
|
|
898
|
+
);
|
|
899
|
+
const settledIds = new Set([
|
|
900
|
+
...this.entries
|
|
901
|
+
.filter((entry) => entry.live && !active.has(entry.runId))
|
|
902
|
+
.map((entry) => entry.runId),
|
|
903
|
+
...[...retained.keys()].filter(
|
|
904
|
+
(runId) =>
|
|
905
|
+
!this.seenRetainedRunIds.has(runId) &&
|
|
906
|
+
!entries.has(runId) &&
|
|
907
|
+
!active.has(runId),
|
|
908
|
+
),
|
|
909
|
+
]);
|
|
910
|
+
for (const runId of settledIds) {
|
|
911
|
+
const persisted = readPersistedWorkflowDetails(runId);
|
|
912
|
+
const details =
|
|
913
|
+
persisted ?? retained.get(runId) ?? entries.get(runId)?.details;
|
|
914
|
+
if (!details) continue;
|
|
915
|
+
if (
|
|
916
|
+
!matchesRunScope(
|
|
917
|
+
details,
|
|
918
|
+
runId,
|
|
919
|
+
this.sessionId,
|
|
920
|
+
this.referencedRunIds,
|
|
921
|
+
this.startedSince,
|
|
922
|
+
!persisted,
|
|
923
|
+
)
|
|
924
|
+
) {
|
|
925
|
+
entries.delete(runId);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
// Recovery operates on a projection, never on the former live owner.
|
|
929
|
+
const recovered = recoverStaleWorkflowDetails({
|
|
930
|
+
...details,
|
|
931
|
+
agents: details.agents.map((agent) => ({ ...agent })),
|
|
932
|
+
});
|
|
933
|
+
entries.set(runId, { runId, details: recovered, live: false });
|
|
934
|
+
this.hydratedRunIds.delete(runId);
|
|
935
|
+
}
|
|
936
|
+
for (const [runId, details] of active) {
|
|
937
|
+
entries.set(runId, { runId, details, live: true });
|
|
938
|
+
}
|
|
939
|
+
this.entries = [...entries.values()].sort(
|
|
940
|
+
(a, b) => b.details.startedAt - a.details.startedAt,
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
for (const runId of retained.keys()) this.seenRetainedRunIds.add(runId);
|
|
828
944
|
if (selected) {
|
|
829
945
|
const index = this.entries.findIndex((e) => e.runId === selected);
|
|
830
946
|
if (index >= 0) this.listIndex = index;
|
|
@@ -839,6 +955,7 @@ export class WorkflowDashboard {
|
|
|
839
955
|
);
|
|
840
956
|
if (refreshed) this.current = refreshed;
|
|
841
957
|
}
|
|
958
|
+
if (this.view === "transcript") this.hydrateCurrent();
|
|
842
959
|
if (this.notice && Date.now() - this.noticeAt > NOTICE_TTL_MS)
|
|
843
960
|
this.notice = undefined;
|
|
844
961
|
}
|
|
@@ -874,7 +991,15 @@ export class WorkflowDashboard {
|
|
|
874
991
|
this.agentIndex = Math.min(this.agentIndex, Math.max(0, agents.length - 1));
|
|
875
992
|
}
|
|
876
993
|
|
|
994
|
+
private hydrateCurrent() {
|
|
995
|
+
const entry = this.current;
|
|
996
|
+
if (!entry || entry.live || this.hydratedRunIds.has(entry.runId)) return;
|
|
997
|
+
hydrateRunArtifacts(entry.runId, entry.details);
|
|
998
|
+
this.hydratedRunIds.add(entry.runId);
|
|
999
|
+
}
|
|
1000
|
+
|
|
877
1001
|
private saveReport() {
|
|
1002
|
+
this.hydrateCurrent();
|
|
878
1003
|
const entry = this.current;
|
|
879
1004
|
if (!entry) return;
|
|
880
1005
|
const target = path.join(runsDir(), entry.runId, "report.md");
|
|
@@ -1015,6 +1140,7 @@ export class WorkflowDashboard {
|
|
|
1015
1140
|
}
|
|
1016
1141
|
|
|
1017
1142
|
private openTranscriptPage() {
|
|
1143
|
+
this.hydrateCurrent();
|
|
1018
1144
|
const transcriptAdapter = new WorkflowTranscriptAdapter();
|
|
1019
1145
|
this.view = "transcript";
|
|
1020
1146
|
this.transcriptPage = new AgentSessionPage(
|