@aeondave/pi-persona 1.17.1 → 1.17.4
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/AGENTS.md +103 -64
- package/CHANGELOG.md +310 -1
- package/README.md +37 -23
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +280 -92
- package/docs/COLLABORATION.md +110 -0
- package/docs/EXPERIENCE.md +52 -17
- package/docs/MONITORS.md +62 -3
- package/docs/README.md +12 -0
- package/docs/REFERENCE.md +139 -38
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +15 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +26 -0
- package/src/bridge.ts +28 -10
- package/src/bus/broker/client.ts +119 -21
- package/src/bus/broker/host.ts +123 -8
- package/src/bus/broker/messages.ts +31 -4
- package/src/bus/contact.ts +113 -38
- package/src/bus/peers.ts +15 -0
- package/src/bus/result-reader.ts +88 -0
- package/src/core/brief.ts +436 -353
- package/src/core/capabilities.ts +1 -1
- package/src/core/config.ts +15 -10
- package/src/core/leg-thinking.ts +57 -0
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/ownership.ts +1 -2
- package/src/core/pi-compat.ts +5 -12
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/core/types.ts +1 -1
- package/src/engine/adapter.ts +95 -27
- package/src/engine/async.ts +213 -155
- package/src/engine/child.ts +12 -4
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +99 -53
- package/src/engine/result-grant.ts +62 -0
- package/src/engine/session-tools.ts +6 -0
- package/src/engine/stream.ts +10 -20
- package/src/engine/worktree.ts +17 -7
- package/src/exocom/collaboration-delivery.ts +29 -0
- package/src/exocom/collaboration.ts +148 -0
- package/src/exocom/envelope.ts +43 -4
- package/src/exocom/gate.ts +7 -2
- package/src/exocom/install.ts +47 -18
- package/src/exocom/ledger.ts +58 -8
- package/src/exocom/plane.ts +32 -1
- package/src/exocom/profile.ts +40 -0
- package/src/exocom/registry.ts +5 -0
- package/src/exocom/status.ts +11 -1
- package/src/exocom/write-guard.ts +7 -0
- package/src/extension/broker-host.ts +61 -9
- package/src/extension/engine.ts +54 -28
- package/src/extension/hooks.ts +952 -868
- package/src/extension/identity.ts +41 -75
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/results.ts +136 -0
- package/src/extension/shared.ts +32 -7
- package/src/extension.ts +2965 -2611
- package/src/loader.ts +11 -3
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +33 -6
- package/src/orchestration/flow-run.ts +160 -32
- package/src/orchestration/flow.ts +20 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/roster.ts +71 -23
- package/src/orchestration/sdk.ts +61 -11
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/orchestration/types.ts +10 -0
- package/src/persona/model-participants.ts +20 -7
- package/src/persona/orchestrate.ts +2 -0
- package/src/persona/persona.ts +8 -4
- package/src/results/archive.ts +281 -0
- package/src/results/record.ts +166 -0
- package/src/results/retrieval.ts +142 -0
- package/src/results/types.ts +128 -0
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +108 -66
- package/src/tools/delegate-tool.ts +349 -283
- package/src/tools/delegate.ts +35 -216
- package/src/tools/exocom-collaboration-input.ts +168 -0
- package/src/tools/exocom-collaboration.ts +315 -0
- package/src/tools/exocom-profile.ts +102 -0
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +35 -22
- package/src/tools/flow.ts +103 -30
- package/src/tools/intercom-tool.ts +202 -128
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +34 -9
- package/src/tools/monitor.ts +58 -6
- package/src/tools/read-result.ts +52 -0
- package/src/tools/timer.ts +34 -15
- package/src/ui/agent-overlay.ts +65 -16
- package/src/ui/agent-tree.ts +127 -19
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/orchestration-launch.ts +49 -0
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +366 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/** Supervisor-facing terminal receipts and the shared fenced page projection. No lifecycle effects. */
|
|
2
|
+
import { fenceUntrusted } from "../core/fence.ts";
|
|
3
|
+
import { readFailure, type ReadReply, type ScopedReader } from "../bus/result-reader.ts";
|
|
4
|
+
import { RESULT_MAX_BODY_CHARS, RESULT_READ_DEFAULT, RESULT_READ_MAX, type ArchiveFailure, type ResultPage, type ResultReadRequest,
|
|
5
|
+
type ResultStorage, type SaveOutcome, type TerminalStatus } from "./types.ts";
|
|
6
|
+
import type { StoredBodyPage } from "./record.ts";
|
|
7
|
+
|
|
8
|
+
export interface ResultReceipt {
|
|
9
|
+
ref?: string;
|
|
10
|
+
storage?: ResultStorage;
|
|
11
|
+
truncated?: boolean;
|
|
12
|
+
warning?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resultReceipt(outcome: SaveOutcome): ResultReceipt {
|
|
16
|
+
if (!outcome.ok) return { warning: `Terminal report not archived (${outcome.reason}).` };
|
|
17
|
+
return {
|
|
18
|
+
ref: outcome.ref, storage: outcome.storage, truncated: outcome.summary.truncated,
|
|
19
|
+
...(outcome.warnings.length > 0 ? { warning: `Terminal storage: ${outcome.warnings.join(", ")}.` } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resultReceiptHint(receipt: ResultReceipt | undefined): string {
|
|
24
|
+
if (!receipt) return "";
|
|
25
|
+
const reference = receipt.ref
|
|
26
|
+
? `${receipt.ref} · ${receipt.storage === "session-entry" ? "native session entry accepted (not an fsync guarantee)" : "memory-only; runtime-only, not durable"}. Read with intercom { action:"result", ref:"${receipt.ref}" }.`
|
|
27
|
+
: "";
|
|
28
|
+
return [reference, receipt.truncated ? "Saved report truncated by archive limits." : "", receipt.warning ?? ""].filter(Boolean).join(" ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Live reports are not restricted to archive body retention; paging still obeys the same UTF-16 contract. */
|
|
32
|
+
export function pageLiveResult(body: string, offset = 0, limit = RESULT_READ_DEFAULT): StoredBodyPage | undefined {
|
|
33
|
+
const splitsPair = (at: number): boolean => at > 0 && at < body.length
|
|
34
|
+
&& /[\uD800-\uDBFF]/.test(body[at - 1]!) && /[\uDC00-\uDFFF]/.test(body[at]!);
|
|
35
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > body.length || splitsPair(offset)
|
|
36
|
+
|| !Number.isInteger(limit) || limit < 2 || limit > RESULT_READ_MAX) return undefined;
|
|
37
|
+
let end = Math.min(body.length, offset + limit);
|
|
38
|
+
if (splitsPair(end)) end--;
|
|
39
|
+
return { body: body.slice(offset, end), offset, endOfStoredBody: end === body.length, ...(end < body.length ? { nextOffset: end } : {}) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Metadata is caller-owned; report and diagnostic text are always independently fenced. */
|
|
43
|
+
export function projectResultPage(page: StoredBodyPage, error?: string): string {
|
|
44
|
+
const cause = error ? `\nFailure detail:\n${fenceUntrusted(error.slice(0, 2048))}` : "";
|
|
45
|
+
const next = page.nextOffset === undefined ? "end of stored body" : `next offset: ${page.nextOffset}`;
|
|
46
|
+
return `Page offset ${page.offset}; ${page.body.length} UTF-16 units; ${next}.${cause}\n${page.body === "(no output)" ? page.body : fenceUntrusted(page.body)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── reference handoff (flow reference mode): scoped reads over the terminal archive ──
|
|
50
|
+
|
|
51
|
+
/** The narrow archive face a leg's reader needs — `TerminalResultArchive` satisfies it
|
|
52
|
+
* structurally. Readers never own, hydrate or mutate archive state; they page through it. */
|
|
53
|
+
export interface ResultArchiveView {
|
|
54
|
+
read(request: ResultReadRequest): ResultPage;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** An archive failure reason mapped onto the reader wire's closed error vocabulary. */
|
|
58
|
+
function readFailureFor(reason: ArchiveFailure["reason"]): ReadReply {
|
|
59
|
+
switch (reason) {
|
|
60
|
+
case "invalid":
|
|
61
|
+
return readFailure("invalid");
|
|
62
|
+
case "busy":
|
|
63
|
+
return readFailure("busy");
|
|
64
|
+
default:
|
|
65
|
+
// missing / ambiguous / corrupt / conflict / quota — every identity or durability
|
|
66
|
+
// problem reads to the child as "the ref is not readable here".
|
|
67
|
+
return readFailure("unavailable");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A scoped reader over the owning session's archive, granted an EXPLICIT ref set (the
|
|
72
|
+
* direct upstream phase needs of THIS leg — nothing else). Owner/capability rechecks at
|
|
73
|
+
* call time are the root's job (the adapter closure); revocation is one-way and final.
|
|
74
|
+
* Reads page the archive's already-stored body; they never rebill, collect live runs, or
|
|
75
|
+
* wake anything. Projection/fencing stays the tool's job (`tools/read-result.ts`). */
|
|
76
|
+
export interface ScopedResultReader extends ScopedReader {
|
|
77
|
+
/** Refs that are not durably readable (missing, unreadable, or memory-only). Empty = healthy. */
|
|
78
|
+
preflight(): { missing: string[] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function makeScopedResultReader(view: ResultArchiveView, granted: readonly string[]): ScopedResultReader {
|
|
82
|
+
const refs = new Set(granted);
|
|
83
|
+
let revoked = false;
|
|
84
|
+
const page = (request: ResultReadRequest): ReadReply => {
|
|
85
|
+
const result = view.read(request);
|
|
86
|
+
if (!result.ok) return readFailureFor(result.reason);
|
|
87
|
+
const s = result.summary;
|
|
88
|
+
const end = result.offset + result.body.length;
|
|
89
|
+
return {
|
|
90
|
+
ok: true, ref: s.ref, body: result.body, offset: result.offset,
|
|
91
|
+
...(end < s.storedChars ? { nextOffset: end } : {}),
|
|
92
|
+
storedChars: s.storedChars, originalChars: s.originalChars, truncated: s.truncated,
|
|
93
|
+
endOfStoredBody: result.endOfStoredBody, status: s.status, provenance: s.provenance,
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
async read(request, signal): Promise<ReadReply> {
|
|
98
|
+
if (revoked) return readFailure("revoked");
|
|
99
|
+
if (signal?.aborted) return readFailure("cancelled");
|
|
100
|
+
if (!refs.has(request.ref)) return readFailure("denied");
|
|
101
|
+
return page(request);
|
|
102
|
+
},
|
|
103
|
+
revoke(): void {
|
|
104
|
+
revoked = true;
|
|
105
|
+
},
|
|
106
|
+
preflight(): { missing: string[] } {
|
|
107
|
+
// Reference handoff is a DURABILITY contract: a memory-only record is not a receipt
|
|
108
|
+
// a later process (or a resumed run) could read back, so it does not pass preflight.
|
|
109
|
+
const missing: string[] = [];
|
|
110
|
+
for (const ref of refs) {
|
|
111
|
+
const result = view.read({ ref, offset: 0, limit: 2 });
|
|
112
|
+
if (!result.ok || result.summary.storage !== "session-entry") missing.push(ref);
|
|
113
|
+
}
|
|
114
|
+
return { missing };
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The full stored body of an archived result, paged out to the archive's own ceiling. Used
|
|
120
|
+
* by a reference-mode RESUME to rebuild a done phase's output from the archive (contract 2). */
|
|
121
|
+
export function readArchivedBody(view: ResultArchiveView, ref: string):
|
|
122
|
+
| { ok: true; body: string; status: TerminalStatus; truncated: boolean }
|
|
123
|
+
| { ok: false; error: string } {
|
|
124
|
+
let offset = 0;
|
|
125
|
+
let body = "";
|
|
126
|
+
let status: TerminalStatus | undefined;
|
|
127
|
+
let truncated = false;
|
|
128
|
+
while (true) {
|
|
129
|
+
const page = view.read({ ref, offset, limit: RESULT_READ_MAX });
|
|
130
|
+
if (!page.ok) return { ok: false, error: `${page.reason}: ${page.message}` };
|
|
131
|
+
status = page.summary.status;
|
|
132
|
+
truncated = page.summary.truncated;
|
|
133
|
+
body += page.body;
|
|
134
|
+
if (page.endOfStoredBody || page.nextOffset === undefined) break;
|
|
135
|
+
if (page.nextOffset <= offset || body.length >= RESULT_MAX_BODY_CHARS) {
|
|
136
|
+
return { ok: false, error: `archive paging did not advance past offset ${page.nextOffset}` };
|
|
137
|
+
}
|
|
138
|
+
offset = page.nextOffset;
|
|
139
|
+
}
|
|
140
|
+
if (status === undefined) return { ok: false, error: "archive returned no status" };
|
|
141
|
+
return { ok: true, body, status, truncated };
|
|
142
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "../core/types.ts";
|
|
2
|
+
|
|
3
|
+
export const TERMINAL_RESULT_ENTRY = "pi-persona-terminal-result";
|
|
4
|
+
export const RESULT_MAX_ENTRIES = 128;
|
|
5
|
+
/** UTF-8 JSON custom-entry DATA only; the host's JSONL envelope is excluded. */
|
|
6
|
+
export const RESULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
export const RESULT_MAX_RECORD_BYTES = 512 * 1024;
|
|
8
|
+
export const RESULT_MAX_BODY_CHARS = 262_144;
|
|
9
|
+
export const RESULT_READ_DEFAULT = 8_000;
|
|
10
|
+
export const RESULT_READ_MAX = 16_000;
|
|
11
|
+
export const RESULT_LIST_DEFAULT = 20;
|
|
12
|
+
export const RESULT_LIST_MAX = 50;
|
|
13
|
+
|
|
14
|
+
export type TerminalStatus = "done" | "failed" | "stopped";
|
|
15
|
+
export type ResultProvenance = "result-output" | "partial-output" | "error";
|
|
16
|
+
export type ResultStorage = "session-entry" | "memory-only";
|
|
17
|
+
export type ResultWarning = "no-session-storage" | "accounting-unavailable" | "invalid-history"
|
|
18
|
+
| "duplicate-history" | "ambiguous-history" | "append-failed" | "body-truncated";
|
|
19
|
+
|
|
20
|
+
/** Cumulative usage only, never a context-window claim or per-token stream. */
|
|
21
|
+
export interface TerminalUsage {
|
|
22
|
+
input: number;
|
|
23
|
+
output: number;
|
|
24
|
+
cacheRead?: number;
|
|
25
|
+
cacheWrite?: number;
|
|
26
|
+
cost?: number;
|
|
27
|
+
turns?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Trusted caller projection, NOT an arbitrary AgentResult. Unknown fields are rejected.
|
|
31
|
+
* Metadata ceilings (UTF-16): originKey 1024, originRunId/agent 128, label 256,
|
|
32
|
+
* model 512, error 2048. Invalid metadata rejects the save rather than silently changing it. */
|
|
33
|
+
export interface TerminalResultInput {
|
|
34
|
+
/** Immutable activation + run/phase identity. Routing labels are not identities. */
|
|
35
|
+
originKey: string;
|
|
36
|
+
originRunId?: string;
|
|
37
|
+
status: TerminalStatus;
|
|
38
|
+
agent: string;
|
|
39
|
+
label?: string;
|
|
40
|
+
model?: string;
|
|
41
|
+
thinkingRequested?: ThinkingLevel;
|
|
42
|
+
thinkingEffective?: ThinkingLevel;
|
|
43
|
+
usage?: TerminalUsage;
|
|
44
|
+
/** Epoch milliseconds. Omit to stamp a NEW record with the injected clock. */
|
|
45
|
+
createdAt?: number;
|
|
46
|
+
body: string;
|
|
47
|
+
provenance: ResultProvenance;
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TerminalResultRecord extends Omit<TerminalResultInput, "createdAt"> {
|
|
52
|
+
version: 1;
|
|
53
|
+
ref: string;
|
|
54
|
+
createdAt: number;
|
|
55
|
+
/** SHA-256 content identity includes the original body, even its discarded suffix.
|
|
56
|
+
* This is collision detection, not authentication of host history. */
|
|
57
|
+
payloadHash: string;
|
|
58
|
+
originalChars: number;
|
|
59
|
+
storedChars: number;
|
|
60
|
+
truncated: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ResultSummary extends Omit<TerminalResultRecord, "body" | "payloadHash"> {
|
|
64
|
+
storage: ResultStorage;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface ResultAccounting {
|
|
68
|
+
/** Includes unknown/malformed/duplicate history and reserved append attempts. */
|
|
69
|
+
chargedEntries: number;
|
|
70
|
+
/** null means the lifetime baseline is unaccountable; persistence is disabled. */
|
|
71
|
+
chargedPayloadBytes: number | null;
|
|
72
|
+
persistenceDisabled: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ArchiveEvidence {
|
|
76
|
+
accounting: ResultAccounting;
|
|
77
|
+
warnings: ResultWarning[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ArchiveFailure extends ArchiveEvidence {
|
|
81
|
+
ok: false;
|
|
82
|
+
reason: "invalid" | "unavailable" | "conflict" | "ambiguous" | "corrupt" | "quota" | "busy";
|
|
83
|
+
message: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export type SaveOutcome = ArchiveFailure | (ArchiveEvidence & {
|
|
87
|
+
ok: true;
|
|
88
|
+
ref: string;
|
|
89
|
+
/** session-entry means append returned normally or data came from native history.
|
|
90
|
+
* It is NOT an fsync/crash-survival guarantee. memory-only is never a durable receipt. */
|
|
91
|
+
storage: ResultStorage;
|
|
92
|
+
summary: ResultSummary;
|
|
93
|
+
idempotent: boolean;
|
|
94
|
+
storageError?: string;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export interface ResultReadRequest { ref: string; offset?: number; limit?: number }
|
|
98
|
+
export type ResultPage = ArchiveFailure | (ArchiveEvidence & {
|
|
99
|
+
ok: true;
|
|
100
|
+
summary: ResultSummary;
|
|
101
|
+
/** Untrusted text; the integration must apply its shared fence on model delivery. */
|
|
102
|
+
body: string;
|
|
103
|
+
offset: number;
|
|
104
|
+
nextOffset?: number;
|
|
105
|
+
/** Reached the end of the STORED body, not necessarily the original report. */
|
|
106
|
+
endOfStoredBody: boolean;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
export interface ResultListRequest { cursor?: number; limit?: number }
|
|
110
|
+
export type ResultSummaryPage = ArchiveFailure | (ArchiveEvidence & {
|
|
111
|
+
ok: true;
|
|
112
|
+
results: ResultSummary[];
|
|
113
|
+
/** Opaque-in-practice append ordinal, not a run id or a branch-local index. */
|
|
114
|
+
nextCursor?: number;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export interface TerminalResultArchiveDeps {
|
|
118
|
+
/** All branches in ONE owning session file. Never read parent/foreign files.
|
|
119
|
+
* History entries are immutable host data. Hydration is read-only and runs once.
|
|
120
|
+
* Recreate the archive on owner change; adapters must reject stale-owner writes. */
|
|
121
|
+
entries: () => readonly unknown[];
|
|
122
|
+
/** Native custom ENTRY, not a model message. May throw after partially committing. */
|
|
123
|
+
append: (customType: string, data: unknown) => void;
|
|
124
|
+
persistent: boolean;
|
|
125
|
+
/** Must return result:<UUID>. The default uses node:crypto.randomUUID(). */
|
|
126
|
+
makeRef?: () => string;
|
|
127
|
+
clock?: () => number;
|
|
128
|
+
}
|
|
@@ -4,7 +4,7 @@ export const TELEMETRY_EVENT_NAME = "pi:telemetry" as const;
|
|
|
4
4
|
export const LEGACY_TELEMETRY_EVENT_NAME = "pi-persona:telemetry" as const;
|
|
5
5
|
export const LEGACY_TELEMETRY_VERSION = 1 as const;
|
|
6
6
|
export const TELEMETRY_PRODUCER_ID = "pi-persona" as const;
|
|
7
|
-
export const TELEMETRY_PRODUCER_VERSION = "1.17.
|
|
7
|
+
export const TELEMETRY_PRODUCER_VERSION = "1.17.4" as const;
|
|
8
8
|
|
|
9
9
|
export type InstanceStatus = "active" | "idle" | "stopped" | "stale" | (string & {});
|
|
10
10
|
export type AgentKind = "subagent" | "delegate" | "council" | "flow" | "phase" | (string & {});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `council` and `flow` as ONE tracked run (the same machinery `delegate` uses): a visible strategy or
|
|
3
|
+
* flow is launched into the {@link AsyncRunTracker}, rooted in the agent tree at `async:<runId>`, and
|
|
4
|
+
* reachable through the stop key `async:<runId>`: `intercom stop`, stop all, and `x` on the run's root row
|
|
5
|
+
* in the f9 overlay all end the whole run. Interactive sessions get a receipt at once and ONE completion
|
|
6
|
+
* follow-up (`renderCompletion`); headless sessions join the same run before the tool returns, so the
|
|
7
|
+
* single turn carries the result.
|
|
8
|
+
*
|
|
9
|
+
* Which semaphore gates what:
|
|
10
|
+
* - `slots` is the launch pool `delegate` already uses (`maxConcurrency`). A council or flow takes ONE
|
|
11
|
+
* slot for the whole run, exactly like one delegate leg, and shows `queued` until it has it.
|
|
12
|
+
* - The strategy SDK's own per-run semaphore (`orchestration/sdk.ts`) gates the legs INSIDE the run, so
|
|
13
|
+
* a council's members never compete with delegate legs for these slots and cannot deadlock on them.
|
|
14
|
+
* A flow parked at a human checkpoint keeps its slot for the wait: the operator owns that wait, and
|
|
15
|
+
* stopping the run releases it.
|
|
16
|
+
* - Token budgets, the child limit and the fork-bomb guard live in that SDK and the engine, untouched.
|
|
17
|
+
*/
|
|
18
|
+
import { END_TURN_NOTE } from "../core/brief.ts";
|
|
19
|
+
import type { ChildUsage } from "../engine/stream.ts";
|
|
20
|
+
import { emptyUsage } from "../engine/stream.ts";
|
|
21
|
+
import type { AgentResult } from "../orchestration/types.ts";
|
|
22
|
+
import type { Semaphore } from "../orchestration/parallel.ts";
|
|
23
|
+
import { madeHeadway, type AsyncRun, type AsyncRunTracker, type IdleCoalescingNotifier, type ProgressReading } from "../engine/async.ts";
|
|
24
|
+
import type { AgentTree } from "../ui/agent-tree.ts";
|
|
25
|
+
import type { ChildUsageLedger } from "../ui/usage.ts";
|
|
26
|
+
|
|
27
|
+
/** What a visible runner is handed when it works for a tracked run (absent ⇒ it roots and stops itself). */
|
|
28
|
+
export interface TrackedRun {
|
|
29
|
+
/** The tree root of this run: `async:<runId>`. Its nodes hang under it. */
|
|
30
|
+
rootId: string;
|
|
31
|
+
/** Fires when the run is stopped — by key, by `intercom stop`, or by the headless tool call's abort. */
|
|
32
|
+
signal: AbortSignal;
|
|
33
|
+
/** Interactive: the launching turn has ended and the supervisor is idle, so a coached leg may block on an
|
|
34
|
+
* ask (the run's engines are built `async`). Headless: the call is parked in the turn and could not answer. */
|
|
35
|
+
background: boolean;
|
|
36
|
+
/** One leg progressed: feeds the run's cumulative tokens, and its stall clock when the leg made headway. */
|
|
37
|
+
onProgress(leg: string, progress: ProgressReading): void;
|
|
38
|
+
/** The run is blocked on the operator (a flow checkpoint) — or released again (`undefined`). */
|
|
39
|
+
onAwaiting(label: string | undefined): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface BackgroundRunDeps {
|
|
43
|
+
tracker: AsyncRunTracker;
|
|
44
|
+
agentTree: AgentTree;
|
|
45
|
+
stopRegistry: Map<string, () => void>;
|
|
46
|
+
/** The launch pool shared with `delegate` (see the module comment). */
|
|
47
|
+
slots: Semaphore;
|
|
48
|
+
completionNotifier: Pick<IdleCoalescingNotifier<AsyncRun>, "discard">;
|
|
49
|
+
startPeek(): void;
|
|
50
|
+
childUsage: ChildUsageLedger;
|
|
51
|
+
publishPersonaCost(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `inline` (headless) skips the status monitor: the launching call joins the run and nobody reads a status card. */
|
|
55
|
+
export interface LaunchMeta { kind: "council" | "flow"; label: string; task: string; inline: boolean }
|
|
56
|
+
|
|
57
|
+
type RunWork = (run: TrackedRun) => Promise<AgentResult>;
|
|
58
|
+
|
|
59
|
+
/** The receipt an interactive `council`/`flow` launch returns. `wake` finishes "you will be woken ONCE ...". */
|
|
60
|
+
export function launchReceipt(kind: LaunchMeta["kind"], id: string, subject: string, wake: string): string {
|
|
61
|
+
return `Launched ${kind} ${id} (${subject}) in the background — you will be woken ONCE ${wake} (${END_TURN_NOTE}).`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface BackgroundRuns {
|
|
65
|
+
/** Launch `work` as one tracked run and return its id. */
|
|
66
|
+
launch(meta: LaunchMeta, work: RunWork): string;
|
|
67
|
+
/** Join launched runs (headless): wait for all of them, stop them if `signal` aborts, drop the follow-ups
|
|
68
|
+
* this return replaces, and bill their usage once. `onSettled` sees each settle as (done, total). */
|
|
69
|
+
settle(ids: readonly string[], signal: AbortSignal | undefined, onSettled?: (done: number, total: number) => void): Promise<{ runs: AsyncRun[]; usage: ChildUsage }>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createBackgroundRuns(d: BackgroundRunDeps): BackgroundRuns {
|
|
73
|
+
function launch(meta: LaunchMeta, work: RunWork): string {
|
|
74
|
+
// `mutates`: a strategy's legs and their tool grants are decided leg by leg inside the run, so the run
|
|
75
|
+
// is classed as a potential writer — the fail-closed answer for a persona's fresh-verification policy
|
|
76
|
+
// (whose refusal says "may mutate", which is all this classification knows).
|
|
77
|
+
const id = d.tracker.launch({ agent: meta.label, task: meta.task, label: meta.label, mutates: true, queued: true }, (onProgress, runId) => {
|
|
78
|
+
const rootId = `async:${runId}`;
|
|
79
|
+
const ac = new AbortController();
|
|
80
|
+
d.stopRegistry.set(rootId, () => ac.abort());
|
|
81
|
+
const legSeen = new Map<string, ProgressReading>();
|
|
82
|
+
const legTokens = new Map<string, number>();
|
|
83
|
+
const snapshot = (output = ""): { output: string; turns: number; tokens: number } => ({ output, turns: 0, tokens: [...legTokens.values()].reduce((a, b) => a + b, 0) });
|
|
84
|
+
const run: TrackedRun = {
|
|
85
|
+
rootId,
|
|
86
|
+
signal: ac.signal,
|
|
87
|
+
background: !meta.inline,
|
|
88
|
+
onProgress: (leg, progress) => {
|
|
89
|
+
// Headway is judged per leg - the run's own snapshot is only their token sum - by the same rule a
|
|
90
|
+
// delegate leg is held to, so one chatty tool in one leg cannot keep the whole run looking alive.
|
|
91
|
+
if (madeHeadway(legSeen.get(leg), progress)) d.tracker.advance(runId);
|
|
92
|
+
legSeen.set(leg, progress);
|
|
93
|
+
legTokens.set(leg, progress.tokens ?? legTokens.get(leg) ?? 0);
|
|
94
|
+
onProgress(snapshot());
|
|
95
|
+
},
|
|
96
|
+
onAwaiting: (label) => {
|
|
97
|
+
d.tracker.setAwaiting(runId, label);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
return d.slots
|
|
101
|
+
.with(async () => {
|
|
102
|
+
d.tracker.markAdmitted(runId);
|
|
103
|
+
const result = await work(run);
|
|
104
|
+
// A ruling or plan that did not complete lives in `result.output`, but a failed or stopped run's
|
|
105
|
+
// report salvages the tracker's `progress.output` — hand it over so that work is not lost.
|
|
106
|
+
if (!result.ok && result.output) onProgress(snapshot(result.output));
|
|
107
|
+
return result;
|
|
108
|
+
}, ac.signal)
|
|
109
|
+
.catch((error: unknown): AgentResult => {
|
|
110
|
+
if (!ac.signal.aborted) throw error;
|
|
111
|
+
return { agent: meta.label, output: "", usage: emptyUsage(), ok: false, error: "run aborted", failureKind: "abort" };
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// Visible while queued, so `x` and the tree's stop work before the pool grants a slot; the visible runner
|
|
115
|
+
// upserts this same row when it starts.
|
|
116
|
+
d.agentTree.add({ id: `async:${id}`, label: meta.label, kind: meta.kind, status: "running", detail: "queued" });
|
|
117
|
+
if (!meta.inline) d.startPeek();
|
|
118
|
+
return id;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function settle(ids: readonly string[], signal: AbortSignal | undefined, onSettled?: (done: number, total: number) => void): Promise<{ runs: AsyncRun[]; usage: ChildUsage }> {
|
|
122
|
+
const runs = ids.flatMap((id) => d.tracker.peek(id) ?? []); // launch() registers each, and a running entry is never evicted
|
|
123
|
+
const stopAll = (): void => {
|
|
124
|
+
for (const id of ids) d.stopRegistry.get(`async:${id}`)?.();
|
|
125
|
+
};
|
|
126
|
+
if (signal?.aborted) stopAll();
|
|
127
|
+
else signal?.addEventListener("abort", stopAll, { once: true });
|
|
128
|
+
try {
|
|
129
|
+
if (runs.some((run) => !run.settled)) {
|
|
130
|
+
await new Promise<void>((resolve) => {
|
|
131
|
+
const off = d.tracker.onComplete(() => {
|
|
132
|
+
const done = runs.filter((run) => run.settled).length;
|
|
133
|
+
onSettled?.(done, runs.length);
|
|
134
|
+
if (done === runs.length) {
|
|
135
|
+
off();
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
signal?.removeEventListener("abort", stopAll);
|
|
143
|
+
}
|
|
144
|
+
d.completionNotifier.discard((pending) => ids.includes(pending.id)); // this return IS the delivery
|
|
145
|
+
const usage = d.childUsage.accountMany(runs.map((run) => ({ key: run.id, usage: run.result?.usage ?? emptyUsage() })));
|
|
146
|
+
d.publishPersonaCost();
|
|
147
|
+
return { runs, usage };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { launch, settle };
|
|
151
|
+
}
|