@fitchmultz/pi-subagents 0.50.0 → 0.52.0
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 +63 -2
- package/README.md +36 -5
- package/dist/agent/child-runtime.js +80 -0
- package/dist/agent/contracts.js +15 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +212 -192
- package/dist/agent/journal-tail.js +115 -156
- package/dist/agent/launch.js +20 -8
- package/dist/agent/legacy.js +65 -23
- package/dist/agent/notifications.js +138 -47
- package/dist/agent/observation-state.js +246 -0
- package/dist/agent/observation-view.js +258 -0
- package/dist/agent/observation.js +350 -0
- package/dist/agent/observe-card.js +243 -0
- package/dist/agent/observe-chrome.js +85 -0
- package/dist/agent/observe-client.js +285 -0
- package/dist/agent/observe-layout.js +65 -0
- package/dist/agent/observe-reading.js +84 -0
- package/dist/agent/observe-screen.js +400 -96
- package/dist/agent/observe-transcript.js +382 -0
- package/dist/agent/observe.js +250 -43
- package/dist/agent/parking.js +253 -0
- package/dist/agent/process.js +228 -28
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +229 -126
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +47 -12
- package/dist/agent/tool-schemas.js +20 -0
- package/dist/agent/typebox.js +10 -5
- package/dist/agent/usage.js +0 -115
- package/dist/agent/view.js +46 -16
- package/dist/agent/wake.js +99 -0
- package/dist/build-info.js +1 -1
- package/dist/intercom-client.d.ts +26 -0
- package/dist/intercom-client.js +45 -0
- package/dist/pi-intercom/agent-dir.d.ts +1 -0
- package/dist/pi-intercom/bridge-session.js +2 -1
- package/dist/pi-intercom/broker/broker.js +62 -43
- package/dist/pi-intercom/broker/client-last.d.ts +24 -0
- package/dist/pi-intercom/broker/client-last.js +42 -5
- package/dist/pi-intercom/broker/client-park.d.ts +9 -0
- package/dist/pi-intercom/broker/client-park.js +21 -0
- package/dist/pi-intercom/broker/client.d.ts +59 -0
- package/dist/pi-intercom/broker/client.js +55 -29
- package/dist/pi-intercom/broker/connection.d.ts +29 -0
- package/dist/pi-intercom/broker/framing.d.ts +17 -0
- package/dist/pi-intercom/broker/framing.js +10 -7
- package/dist/pi-intercom/broker/guard.js +23 -0
- package/dist/pi-intercom/broker/last.d.ts +30 -0
- package/dist/pi-intercom/broker/last.js +87 -40
- package/dist/pi-intercom/broker/parked.js +107 -0
- package/dist/pi-intercom/broker/paths.d.ts +13 -0
- package/dist/pi-intercom/broker/paths.js +35 -12
- package/dist/pi-intercom/broker/pid.js +5 -1
- package/dist/pi-intercom/broker/server-connection.js +20 -1
- package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
- package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
- package/dist/pi-intercom/broker/targets.d.ts +9 -0
- package/dist/pi-intercom/connection.js +71 -20
- package/dist/pi-intercom/identity.js +6 -5
- package/dist/pi-intercom/inbound-delivery.js +16 -60
- package/dist/pi-intercom/inbound-journal.js +83 -6
- package/dist/pi-intercom/inbound-record.js +18 -1
- package/dist/pi-intercom/inbound.js +22 -37
- package/dist/pi-intercom/index.js +41 -0
- package/dist/pi-intercom/last-message.js +4 -16
- package/dist/pi-intercom/lifecycle.js +88 -3
- package/dist/pi-intercom/outbound.js +5 -15
- package/dist/pi-intercom/reply-tracker.js +7 -6
- package/dist/pi-intercom/reply-wait.js +2 -2
- package/dist/pi-intercom/session-targets.d.ts +24 -0
- package/dist/pi-intercom/tool-registration.js +4 -1
- package/dist/pi-intercom/tool-results.js +5 -7
- package/dist/pi-intercom/tool-schemas.js +12 -26
- package/dist/pi-intercom/types.d.ts +212 -0
- package/dist/pi-intercom/types.js +54 -3
- package/dist/pi-intercom/validation.d.ts +12 -0
- package/dist/pi-intercom/validation.js +3 -0
- package/dist/shared/bounded-json.js +151 -0
- package/dist/shared/compact-cards.js +66 -0
- package/dist/shared/journal-frames.js +3 -6
- package/dist/shared/json-projection.js +267 -129
- package/dist/shared/native-journal.js +123 -56
- package/dist/shared/parking.js +12 -0
- package/dist/shared/post-exit-stdio-guard.js +120 -7
- package/dist/shared/tui-action.js +29 -0
- package/dist/shared/unknown.d.ts +9 -0
- package/docs/code-quality.md +5 -3
- package/docs/development.md +42 -3
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +228 -7
- package/docs/performance.md +118 -14
- package/docs/reference.md +168 -20
- package/docs/retired-history-maintenance.md +252 -0
- package/package.json +28 -15
- package/scripts/archive-retired-history.mjs +464 -0
- package/scripts/build.mjs +48 -28
- package/scripts/measure-size.mjs +1 -1
- package/scripts/native-package-smoke.mjs +153 -12
- package/scripts/package-smoke.mjs +80 -11
- package/scripts/prepare.mjs +2 -2
- package/scripts/real-pi-live.mjs +377 -0
- package/scripts/real-pi-processes.mjs +6 -1
- package/scripts/real-pi-smoke.mjs +6 -282
- package/scripts/run-tests.mjs +4 -2
- package/scripts/typescript.mjs +35 -0
- package/skills/pi-intercom/SKILL.md +2 -1
- package/skills/pi-subagents/SKILL.md +23 -5
- package/src/agent/child-runtime.ts +105 -0
- package/src/agent/contracts.ts +18 -34
- package/src/agent/host.ts +39 -20
- package/src/agent/index.ts +234 -207
- package/src/agent/journal-tail.ts +141 -153
- package/src/agent/launch.ts +33 -9
- package/src/agent/legacy.ts +85 -31
- package/src/agent/notifications.ts +192 -67
- package/src/agent/observation-state.ts +277 -0
- package/src/agent/observation-view.ts +267 -0
- package/src/agent/observation.ts +409 -0
- package/src/agent/observe-card.ts +304 -0
- package/src/agent/observe-chrome.ts +170 -0
- package/src/agent/observe-client.ts +318 -0
- package/src/agent/observe-layout.ts +79 -0
- package/src/agent/observe-reading.ts +97 -0
- package/src/agent/observe-screen.ts +443 -129
- package/src/agent/observe-transcript.ts +434 -0
- package/src/agent/observe.ts +304 -71
- package/src/agent/parking.ts +341 -0
- package/src/agent/process.ts +280 -37
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +227 -149
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +44 -11
- package/src/agent/tool-schemas.ts +33 -0
- package/src/agent/typebox.ts +12 -7
- package/src/agent/usage.ts +0 -130
- package/src/agent/view.ts +70 -20
- package/src/agent/wake.ts +107 -0
- package/src/intercom-client.ts +108 -0
- package/src/pi-intercom/bridge-session.ts +2 -2
- package/src/pi-intercom/broker/broker.ts +64 -53
- package/src/pi-intercom/broker/client-last.ts +65 -12
- package/src/pi-intercom/broker/client-park.ts +23 -0
- package/src/pi-intercom/broker/client.ts +63 -45
- package/src/pi-intercom/broker/framing.ts +11 -8
- package/src/pi-intercom/broker/guard.ts +33 -0
- package/src/pi-intercom/broker/last.ts +131 -43
- package/src/pi-intercom/broker/parked.ts +130 -0
- package/src/pi-intercom/broker/paths.ts +39 -12
- package/src/pi-intercom/broker/pid.ts +6 -1
- package/src/pi-intercom/broker/server-connection.ts +27 -2
- package/src/pi-intercom/connection.ts +103 -39
- package/src/pi-intercom/identity.ts +8 -5
- package/src/pi-intercom/inbound-delivery.ts +20 -66
- package/src/pi-intercom/inbound-journal.ts +103 -7
- package/src/pi-intercom/inbound-record.ts +21 -4
- package/src/pi-intercom/inbound.ts +32 -49
- package/src/pi-intercom/index.ts +44 -0
- package/src/pi-intercom/last-message.ts +11 -20
- package/src/pi-intercom/lifecycle.ts +117 -14
- package/src/pi-intercom/outbound.ts +7 -16
- package/src/pi-intercom/reply-tracker.ts +8 -6
- package/src/pi-intercom/reply-wait.ts +3 -2
- package/src/pi-intercom/runtime-types.ts +0 -1
- package/src/pi-intercom/tool-registration.ts +4 -1
- package/src/pi-intercom/tool-results.ts +10 -9
- package/src/pi-intercom/tool-schemas.ts +12 -44
- package/src/pi-intercom/types.ts +115 -4
- package/src/pi-intercom/validation.ts +4 -0
- package/src/shared/bounded-json.ts +162 -0
- package/src/shared/compact-cards.ts +118 -0
- package/src/shared/journal-frames.ts +3 -19
- package/src/shared/json-projection.ts +267 -147
- package/src/shared/native-journal.ts +157 -75
- package/src/shared/parking.ts +22 -0
- package/src/shared/post-exit-stdio-guard.ts +165 -7
- package/src/shared/tui-action.ts +39 -0
- package/tsconfig.client.json +8 -0
- package/tsconfig.json +1 -0
- package/src/shared/stream-json.d.ts +0 -10
package/src/agent/usage.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { Usage } from "@earendil-works/pi-ai";
|
|
3
1
|
import * as path from "node:path";
|
|
4
2
|
import * as fs from "node:fs";
|
|
5
3
|
import { journalStamp } from "../shared/native-journal.ts";
|
|
6
|
-
import { isDeepStrictEqual } from "node:util";
|
|
7
4
|
import { nativeProjection } from "../shared/journal-projections.ts";
|
|
8
5
|
import { scanJournal } from "../shared/journal-frames.ts";
|
|
9
|
-
import type { Projection } from "../shared/json-projection.ts";
|
|
10
6
|
import { isRecord } from "../shared/unknown.ts";
|
|
11
7
|
import { Assert } from "./typebox.ts";
|
|
12
8
|
import {
|
|
@@ -18,132 +14,6 @@ import {
|
|
|
18
14
|
} from "./contracts.ts";
|
|
19
15
|
import { read, runDirectory, write } from "./storage.ts";
|
|
20
16
|
|
|
21
|
-
export function sumUsage(values: readonly Contribution[]): Usage {
|
|
22
|
-
const total: Usage = {
|
|
23
|
-
input: 0,
|
|
24
|
-
output: 0,
|
|
25
|
-
cacheRead: 0,
|
|
26
|
-
cacheWrite: 0,
|
|
27
|
-
totalTokens: 0,
|
|
28
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
29
|
-
};
|
|
30
|
-
for (const { usage } of values) {
|
|
31
|
-
for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"] as const) {
|
|
32
|
-
total[key] += usage[key];
|
|
33
|
-
}
|
|
34
|
-
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"] as const) {
|
|
35
|
-
total.cost[key] += usage.cost[key];
|
|
36
|
-
}
|
|
37
|
-
for (const key of ["reasoning", "cacheWrite1h"] as const) {
|
|
38
|
-
if (usage[key] !== undefined) {
|
|
39
|
-
total[key] = (total[key] ?? 0) + usage[key];
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return total;
|
|
44
|
-
}
|
|
45
|
-
function intents(details: unknown): Contribution[] | undefined {
|
|
46
|
-
if (!isRecord(details) || details.agentUsage === undefined) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const values: unknown = details.agentUsage;
|
|
50
|
-
Assert(contributionsSchema, values);
|
|
51
|
-
return values;
|
|
52
|
-
}
|
|
53
|
-
function publishedContributions(entry: unknown): Contribution[] {
|
|
54
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const message = entry.message;
|
|
58
|
-
if (
|
|
59
|
-
message.role !== "toolResult" ||
|
|
60
|
-
(message.toolName !== "delegate" && message.toolName !== "agent_runs")
|
|
61
|
-
) {
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
const values = intents(message.details);
|
|
65
|
-
if (values === undefined || !isDeepStrictEqual(sumUsage(values), message.usage)) {
|
|
66
|
-
return [];
|
|
67
|
-
}
|
|
68
|
-
return values;
|
|
69
|
-
}
|
|
70
|
-
const receiptProjection: Projection = (keys) => {
|
|
71
|
-
if (keys.length === 0 || keys[0] === "type") {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
if (keys[0] !== "message") {
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
if (keys.length === 1 || ["role", "toolName", "usage"].includes(String(keys[1]))) {
|
|
78
|
-
return true;
|
|
79
|
-
}
|
|
80
|
-
return keys[1] === "details" && (keys.length === 2 || keys[2] === "agentUsage");
|
|
81
|
-
};
|
|
82
|
-
function priorUsage(ctx: ExtensionContext): Map<string, Contribution> {
|
|
83
|
-
const values = new Map<string, Contribution>();
|
|
84
|
-
const file = ctx.sessionManager.getSessionFile();
|
|
85
|
-
if (file !== undefined) {
|
|
86
|
-
scanJournal(
|
|
87
|
-
file,
|
|
88
|
-
receiptProjection,
|
|
89
|
-
({ value: entry }) => {
|
|
90
|
-
for (const value of publishedContributions(entry)) {
|
|
91
|
-
values.set(value.id, value);
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
{ policy: "live", requireNewline: true },
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
for (const entry of ctx.sessionManager.getEntries()) {
|
|
98
|
-
for (const value of publishedContributions(entry)) {
|
|
99
|
-
values.set(value.id, value);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return values;
|
|
103
|
-
}
|
|
104
|
-
function uncharged(
|
|
105
|
-
values: readonly Contribution[],
|
|
106
|
-
prior: Map<string, Contribution>,
|
|
107
|
-
): Contribution[] {
|
|
108
|
-
const pending: Contribution[] = [];
|
|
109
|
-
for (const value of values) {
|
|
110
|
-
const earlier = prior.get(value.id);
|
|
111
|
-
if (earlier && !isDeepStrictEqual(earlier, value)) {
|
|
112
|
-
throw new Error(`Conflicting native usage contribution ${value.id}`);
|
|
113
|
-
}
|
|
114
|
-
if (!earlier) {
|
|
115
|
-
pending.push(value);
|
|
116
|
-
prior.set(value.id, value);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return pending;
|
|
120
|
-
}
|
|
121
|
-
export function registerUsage(pi: ExtensionAPI): void {
|
|
122
|
-
pi.on("message_end", (event, ctx) => {
|
|
123
|
-
const message = event.message;
|
|
124
|
-
if (
|
|
125
|
-
message.role !== "toolResult" ||
|
|
126
|
-
(message.toolName !== "delegate" && message.toolName !== "agent_runs")
|
|
127
|
-
) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
const values = intents(message.details);
|
|
131
|
-
if (values === undefined || !isRecord(message.details)) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const pending = uncharged(values, priorUsage(ctx));
|
|
135
|
-
const { usage: _usage, ...rest } = message;
|
|
136
|
-
// Finalized native tool messages are serialized even for concurrent tools.
|
|
137
|
-
// Preparing or discarding a result cannot reserve a charge.
|
|
138
|
-
return {
|
|
139
|
-
message: {
|
|
140
|
-
...rest,
|
|
141
|
-
details: { ...message.details, agentUsage: pending },
|
|
142
|
-
...(pending.length > 0 ? { usage: sumUsage(pending) } : {}),
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
17
|
function billing(entry: Record<string, unknown>) {
|
|
148
18
|
if (entry.checkpoint === true) {
|
|
149
19
|
return;
|
package/src/agent/view.ts
CHANGED
|
@@ -2,16 +2,16 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { readOutputPage } from "../shared/output-page.ts";
|
|
5
|
-
import { actor, alive } from "./process.ts";
|
|
5
|
+
import { actor, alive, observeAlive } from "./process.ts";
|
|
6
6
|
import {
|
|
7
7
|
admissionSchema,
|
|
8
8
|
commandSchema,
|
|
9
9
|
exitSchema,
|
|
10
10
|
successorSchema,
|
|
11
11
|
type Launch,
|
|
12
|
-
type RunsInput,
|
|
13
12
|
type Result,
|
|
14
13
|
} from "./contracts.ts";
|
|
14
|
+
import type { RunsInput } from "./tool-schemas.ts";
|
|
15
15
|
import { conversation, read, runDirectory } from "./storage.ts";
|
|
16
16
|
import { published } from "./notifications.ts";
|
|
17
17
|
import { readCanonicalResult, readResult } from "./usage.ts";
|
|
@@ -56,16 +56,37 @@ interface RunView extends Omit<RunSummary, "result"> {
|
|
|
56
56
|
result?: Result;
|
|
57
57
|
output: ReturnType<typeof outputView>;
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
type Receipt =
|
|
60
|
+
| { readonly receipt: "launch"; readonly note: string }
|
|
61
|
+
| { readonly receipt: "continuation" | "existing-successor" }
|
|
62
|
+
| { readonly receipt: "guidance"; readonly command: string; readonly note: string }
|
|
63
|
+
| { readonly stopRequest?: string; readonly stopAcknowledged: false; readonly note: string };
|
|
64
|
+
|
|
65
|
+
function toolResult(value: unknown, text: string): ToolReply {
|
|
60
66
|
return { content: [{ type: "text" as const, text }], details: value };
|
|
61
67
|
}
|
|
62
|
-
function
|
|
68
|
+
function receiptResult(result: Result | undefined) {
|
|
69
|
+
return result === undefined
|
|
70
|
+
? undefined
|
|
71
|
+
: {
|
|
72
|
+
id: result.id,
|
|
73
|
+
owner: result.owner,
|
|
74
|
+
status: result.status,
|
|
75
|
+
endedAt: result.endedAt,
|
|
76
|
+
terminalEntryId: result.terminalEntryId,
|
|
77
|
+
error: result.error,
|
|
78
|
+
accountingError: result.accountingError,
|
|
79
|
+
accountingPending: result.accountingPending,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** `observed` holds batched liveness by actor token; any other actor gets a fresh check. */
|
|
83
|
+
function processView(base: string, launch: Launch, observed?: ReadonlyMap<string, boolean>) {
|
|
63
84
|
const identity = actor(base, launch);
|
|
64
85
|
const exit = identity
|
|
65
86
|
? read(path.join(conversation(base, launch), `exit-${identity.token}.json`), exitSchema)
|
|
66
87
|
: undefined;
|
|
67
88
|
return {
|
|
68
|
-
live: identity !== undefined && alive(identity),
|
|
89
|
+
live: identity !== undefined && (observed?.get(identity.token) ?? alive(identity)),
|
|
69
90
|
exit: exit ?? null,
|
|
70
91
|
exitKnown: exit !== undefined,
|
|
71
92
|
};
|
|
@@ -120,10 +141,14 @@ function metadata(
|
|
|
120
141
|
base: string,
|
|
121
142
|
launch: Launch,
|
|
122
143
|
result: Result | undefined,
|
|
123
|
-
options: {
|
|
144
|
+
options: {
|
|
145
|
+
ctx?: ExtensionContext;
|
|
146
|
+
guidance: Guidance[];
|
|
147
|
+
observed?: ReadonlyMap<string, boolean>;
|
|
148
|
+
},
|
|
124
149
|
): Omit<RunView, "output"> {
|
|
125
150
|
const directory = runDirectory(base, launch.id);
|
|
126
|
-
const process = processView(base, launch);
|
|
151
|
+
const process = processView(base, launch, options.observed);
|
|
127
152
|
const outputFile = path.join(directory, "output.txt");
|
|
128
153
|
const admission = read(
|
|
129
154
|
path.join(conversation(base, launch), "admissions", `${launch.id}.json`),
|
|
@@ -156,16 +181,22 @@ function metadata(
|
|
|
156
181
|
guidance: options.guidance,
|
|
157
182
|
};
|
|
158
183
|
}
|
|
159
|
-
|
|
184
|
+
async function summaries(
|
|
160
185
|
base: string,
|
|
161
186
|
launches: readonly Launch[],
|
|
162
187
|
ctx?: ExtensionContext,
|
|
163
|
-
): RunSummary[] {
|
|
188
|
+
): Promise<RunSummary[]> {
|
|
164
189
|
const guidance = guidanceView(base, launches);
|
|
190
|
+
const identities = [
|
|
191
|
+
...new Map(launches.map((launch) => [launch.rootId, actor(base, launch)])).values(),
|
|
192
|
+
].filter((identity) => identity !== undefined);
|
|
193
|
+
const verdicts = await observeAlive(identities);
|
|
194
|
+
const observed = new Map(identities.map((identity, index) => [identity.token, verdicts[index]]));
|
|
165
195
|
return launches.map((launch) => {
|
|
166
196
|
const view = metadata(base, launch, readCanonicalResult(base, launch), {
|
|
167
197
|
ctx,
|
|
168
198
|
guidance: guidance.get(launch.id) ?? [],
|
|
199
|
+
observed,
|
|
169
200
|
});
|
|
170
201
|
return {
|
|
171
202
|
...view,
|
|
@@ -180,7 +211,7 @@ export function summaries(
|
|
|
180
211
|
};
|
|
181
212
|
});
|
|
182
213
|
}
|
|
183
|
-
|
|
214
|
+
function projection(
|
|
184
215
|
base: string,
|
|
185
216
|
launch: Launch,
|
|
186
217
|
ctx?: ExtensionContext,
|
|
@@ -195,6 +226,32 @@ export function projection(
|
|
|
195
226
|
output: outputView(view.outputFile, input),
|
|
196
227
|
};
|
|
197
228
|
}
|
|
229
|
+
export function renderReceipt(
|
|
230
|
+
base: string,
|
|
231
|
+
launch: Launch,
|
|
232
|
+
ctx: ExtensionContext | undefined,
|
|
233
|
+
receipt: Receipt,
|
|
234
|
+
): ToolReply {
|
|
235
|
+
const details = { ...projection(base, launch, ctx), ...receipt };
|
|
236
|
+
const model = { ...details, result: receiptResult(details.result) };
|
|
237
|
+
return toolResult(details, JSON.stringify(model, null, 2));
|
|
238
|
+
}
|
|
239
|
+
export async function renderInventory(
|
|
240
|
+
base: string,
|
|
241
|
+
launches: readonly Launch[],
|
|
242
|
+
ctx: ExtensionContext | undefined,
|
|
243
|
+
input: Pick<RunsInput, "offset">,
|
|
244
|
+
): Promise<ToolReply> {
|
|
245
|
+
const offset = input.offset ?? 0;
|
|
246
|
+
const runs = await summaries(base, launches.slice(offset, offset + 50), ctx);
|
|
247
|
+
const details = {
|
|
248
|
+
runs,
|
|
249
|
+
offset,
|
|
250
|
+
nextOffset: offset + runs.length < launches.length ? offset + runs.length : undefined,
|
|
251
|
+
total: launches.length,
|
|
252
|
+
};
|
|
253
|
+
return toolResult(details, JSON.stringify(details, null, 2));
|
|
254
|
+
}
|
|
198
255
|
function receiptText(details: RunView): string {
|
|
199
256
|
return details.nativeReceipt !== null
|
|
200
257
|
? `Native completion receipt: published (${details.nativeReceipt}); publication does not prove model consumption.`
|
|
@@ -225,20 +282,13 @@ function resultText(details: RunView): string {
|
|
|
225
282
|
`Native journal: ${details.sessionFile}`,
|
|
226
283
|
].join("\n");
|
|
227
284
|
}
|
|
228
|
-
export function
|
|
285
|
+
export function renderInspection(
|
|
229
286
|
base: string,
|
|
230
287
|
launch: Launch,
|
|
231
288
|
ctx?: ExtensionContext,
|
|
232
|
-
input: Pick<RunsInput, "offset" | "length">
|
|
289
|
+
input: Pick<RunsInput, "offset" | "length"> = {},
|
|
233
290
|
): ToolReply {
|
|
234
|
-
const
|
|
235
|
-
const details = {
|
|
236
|
-
...view,
|
|
237
|
-
receivedResult: input.receivedResult === true,
|
|
238
|
-
...(input.receivedResult === true && view.result?.contributions !== undefined
|
|
239
|
-
? { agentUsage: view.result.contributions }
|
|
240
|
-
: {}),
|
|
241
|
-
};
|
|
291
|
+
const details = projection(base, launch, ctx, input);
|
|
242
292
|
return {
|
|
243
293
|
...toolResult(details, resultText(details)),
|
|
244
294
|
isError: details.result?.status === "failed",
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { journalStamp } from "../shared/native-journal.ts";
|
|
3
|
+
import { missing } from "./storage.ts";
|
|
4
|
+
|
|
5
|
+
/** Identity and change stamp of a path; "" when absent. */
|
|
6
|
+
export function pathStamp(file: string): string {
|
|
7
|
+
try {
|
|
8
|
+
const stat = fs.statSync(file, { bigint: true, throwIfNoEntry: false });
|
|
9
|
+
return stat === undefined ? "" : journalStamp(stat);
|
|
10
|
+
} catch {
|
|
11
|
+
return "unreadable";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `armed`: hints start now, so reconcile anything published earlier.
|
|
17
|
+
* `changed`: the directory's own entries changed since the previous check, which also
|
|
18
|
+
* covers hints the OS dropped.
|
|
19
|
+
*/
|
|
20
|
+
export type WatchState = "unwatched" | "armed" | "changed" | "watching";
|
|
21
|
+
|
|
22
|
+
/** Native change hints for one directory identity; owners still reconcile canonical files. */
|
|
23
|
+
export class DirectoryWatch {
|
|
24
|
+
private watcher?: fs.FSWatcher;
|
|
25
|
+
private armed = "";
|
|
26
|
+
private checked = "";
|
|
27
|
+
private failed = false;
|
|
28
|
+
private readonly directory: string;
|
|
29
|
+
private readonly relevant: (file: string | null) => boolean;
|
|
30
|
+
private readonly wake: () => void;
|
|
31
|
+
constructor(directory: string, relevant: (file: string | null) => boolean, wake: () => void) {
|
|
32
|
+
this.directory = directory;
|
|
33
|
+
this.relevant = relevant;
|
|
34
|
+
this.wake = wake;
|
|
35
|
+
}
|
|
36
|
+
ensure(): WatchState {
|
|
37
|
+
const stat = fs.statSync(this.directory, { bigint: true, throwIfNoEntry: false });
|
|
38
|
+
const identity = stat?.isDirectory() === true ? `${stat.dev}:${stat.ino}` : "";
|
|
39
|
+
const checked = this.checked;
|
|
40
|
+
this.checked = stat === undefined ? "" : `${identity}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
41
|
+
if (this.watcher !== undefined && identity === this.armed) {
|
|
42
|
+
return checked === this.checked ? "watching" : "changed";
|
|
43
|
+
}
|
|
44
|
+
// A replaced directory leaves an inode watch silent on Linux.
|
|
45
|
+
this.close();
|
|
46
|
+
return identity === "" || this.failed ? "unwatched" : this.arm(identity);
|
|
47
|
+
}
|
|
48
|
+
private arm(identity: string): WatchState {
|
|
49
|
+
try {
|
|
50
|
+
const watcher = fs.watch(this.directory, { persistent: false }, (_event, file) => {
|
|
51
|
+
if (watcher === this.watcher && this.relevant(file)) {
|
|
52
|
+
this.wake();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
watcher.on("error", () => {
|
|
56
|
+
if (watcher === this.watcher) {
|
|
57
|
+
this.failed = true;
|
|
58
|
+
this.close();
|
|
59
|
+
this.wake();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
this.watcher = watcher;
|
|
63
|
+
this.armed = identity;
|
|
64
|
+
return "armed";
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Unsupported platforms and watch limits keep their owner's polling fallback.
|
|
67
|
+
this.failed = !missing(error);
|
|
68
|
+
return "unwatched";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
close(): void {
|
|
72
|
+
this.watcher?.close();
|
|
73
|
+
this.watcher = undefined;
|
|
74
|
+
this.armed = "";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A coalesced wake for one owner loop; raised until that loop takes it. */
|
|
79
|
+
export class Wake {
|
|
80
|
+
private raised = true;
|
|
81
|
+
private resolve?: () => void;
|
|
82
|
+
readonly raise = (): void => {
|
|
83
|
+
this.raised = true;
|
|
84
|
+
this.resolve?.();
|
|
85
|
+
};
|
|
86
|
+
take(): boolean {
|
|
87
|
+
const raised = this.raised;
|
|
88
|
+
this.raised = false;
|
|
89
|
+
return raised;
|
|
90
|
+
}
|
|
91
|
+
/** Returns at once for a wake raised since the last take. */
|
|
92
|
+
async sleep(ms: number): Promise<void> {
|
|
93
|
+
if (this.raised) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const { promise, resolve } = Promise.withResolvers<"woken" | "elapsed">();
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
resolve("elapsed");
|
|
99
|
+
}, ms);
|
|
100
|
+
this.resolve = () => {
|
|
101
|
+
resolve("woken");
|
|
102
|
+
};
|
|
103
|
+
await promise;
|
|
104
|
+
clearTimeout(timer);
|
|
105
|
+
this.resolve = undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IntercomClient as BrokerClient,
|
|
3
|
+
type IntercomClientOptions as BrokerOptions,
|
|
4
|
+
} from "./pi-intercom/broker/client.ts";
|
|
5
|
+
import {
|
|
6
|
+
isDeliveryGuard,
|
|
7
|
+
type DeliveryGuard,
|
|
8
|
+
type LastMessageQuery,
|
|
9
|
+
type LastMessageResult,
|
|
10
|
+
type ReceiptQuery,
|
|
11
|
+
type ReceiptResult,
|
|
12
|
+
type SendOptions,
|
|
13
|
+
type SendResult,
|
|
14
|
+
type SessionInfo,
|
|
15
|
+
} from "./pi-intercom/types.ts";
|
|
16
|
+
import { isRecord } from "./shared/unknown.ts";
|
|
17
|
+
|
|
18
|
+
export type {
|
|
19
|
+
ContextTarget,
|
|
20
|
+
DeliveryGuard,
|
|
21
|
+
LastMessageQuery,
|
|
22
|
+
LastMessageResult,
|
|
23
|
+
MessageDelivery,
|
|
24
|
+
NativeReceipt,
|
|
25
|
+
PublicationReplyHint,
|
|
26
|
+
ReceiptQuery,
|
|
27
|
+
ReceiptResult,
|
|
28
|
+
SendResult,
|
|
29
|
+
SessionInfo,
|
|
30
|
+
} from "./pi-intercom/types.ts";
|
|
31
|
+
|
|
32
|
+
export type GuardedSendOptions = Omit<
|
|
33
|
+
SendOptions,
|
|
34
|
+
"messageId" | "guard" | "expectsReply" | "replyTo" | "human" | "topic" | "passive" | "delivery"
|
|
35
|
+
> & {
|
|
36
|
+
readonly messageId: string;
|
|
37
|
+
readonly guard: DeliveryGuard;
|
|
38
|
+
readonly expectsReply: false;
|
|
39
|
+
readonly delivery: "queue" | "steer" | "passive";
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface IntercomClientOptions extends BrokerOptions {
|
|
43
|
+
readonly spawn: false;
|
|
44
|
+
readonly session: Omit<
|
|
45
|
+
SessionInfo,
|
|
46
|
+
"id" | "guardedDelivery" | "applicationGuard" | "topics" | "subscriptions"
|
|
47
|
+
>;
|
|
48
|
+
readonly requestedId?: string;
|
|
49
|
+
readonly onError?: (error: Readonly<Error>) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IntercomClient {
|
|
53
|
+
readonly sessionId: string | null;
|
|
54
|
+
readonly isConnected: () => boolean;
|
|
55
|
+
readonly listSessions: () => Promise<readonly SessionInfo[]>;
|
|
56
|
+
readonly send: (to: string, options: GuardedSendOptions) => Promise<SendResult>;
|
|
57
|
+
readonly last: (to: string, query?: LastMessageQuery) => Promise<LastMessageResult>;
|
|
58
|
+
readonly receipt: (to: string, query: ReceiptQuery) => Promise<ReceiptResult>;
|
|
59
|
+
readonly disconnect: () => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function requireNoSpawn(value: unknown): void {
|
|
63
|
+
if (value !== false) {
|
|
64
|
+
throw new Error("The supported Intercom client requires spawn:false.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function guardedSend(to: string, input: unknown): boolean {
|
|
69
|
+
return (
|
|
70
|
+
isRecord(input) &&
|
|
71
|
+
isDeliveryGuard(input.guard) &&
|
|
72
|
+
to === input.guard.peerId &&
|
|
73
|
+
typeof input.messageId === "string" &&
|
|
74
|
+
input.expectsReply === false &&
|
|
75
|
+
input.delivery !== undefined
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Connect to an existing native broker. No Pi runtime, broker spawn or automatic retry. */
|
|
80
|
+
export async function connectIntercom(options: IntercomClientOptions): Promise<IntercomClient> {
|
|
81
|
+
requireNoSpawn(options.spawn);
|
|
82
|
+
const client = new BrokerClient(options);
|
|
83
|
+
client.on("error", (error: Readonly<Error>) => options.onError?.(error));
|
|
84
|
+
try {
|
|
85
|
+
await client.connect(options.session, options.requestedId);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
await client.disconnect();
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
get sessionId() {
|
|
92
|
+
return client.sessionId;
|
|
93
|
+
},
|
|
94
|
+
isConnected: () => client.isConnected(),
|
|
95
|
+
listSessions: () => client.listSessions(),
|
|
96
|
+
async send(to: string, input: GuardedSendOptions) {
|
|
97
|
+
if (!guardedSend(to, input)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"An exact guarded nonblocking send with an original message ID is required.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return await client.send(to, input);
|
|
103
|
+
},
|
|
104
|
+
last: (to: string, query?: LastMessageQuery) => client.last(to, query),
|
|
105
|
+
receipt: (to: string, query: ReceiptQuery) => client.receipt(to, query),
|
|
106
|
+
disconnect: () => client.disconnect(),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -6,7 +6,7 @@ import type { Message, SendResult, SessionInfo } from "./types.ts";
|
|
|
6
6
|
import type { Identity } from "./bridge-config.ts";
|
|
7
7
|
import { Fault, cancelled, log, wait } from "./bridge-protocol.ts";
|
|
8
8
|
import { Mailbox } from "./bridge-mailbox.ts";
|
|
9
|
-
import type
|
|
9
|
+
import { hasErrorCode, type UnknownRecord } from "../shared/unknown.ts";
|
|
10
10
|
import type { Operation } from "./bridge-routes.ts";
|
|
11
11
|
import type { RequestContext } from "./bridge-http.ts";
|
|
12
12
|
import { resolveBridgeTarget, bridgeSessionList } from "./bridge-target.ts";
|
|
@@ -272,7 +272,7 @@ export class Session {
|
|
|
272
272
|
peers = await wait(this.client.listSessions(), signal);
|
|
273
273
|
} catch (error) {
|
|
274
274
|
cancelled(signal);
|
|
275
|
-
if (error
|
|
275
|
+
if (hasErrorCode(error, "list_timeout")) {
|
|
276
276
|
throw new Fault(504, "list_timeout", "Broker session list deadline exceeded.");
|
|
277
277
|
}
|
|
278
278
|
throw new Fault(503, "broker_unavailable", "Broker session list failed.");
|