@fitchmultz/pi-subagents 0.50.0 → 0.51.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 +51 -2
- package/README.md +36 -5
- package/dist/agent/contracts.js +6 -24
- package/dist/agent/host.js +3 -16
- package/dist/agent/index.js +207 -173
- 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 +346 -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/process.js +198 -25
- package/dist/agent/receipts.js +92 -20
- package/dist/agent/runner.js +125 -20
- package/dist/agent/shell.js +2 -1
- package/dist/agent/storage.js +29 -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 +27 -21
- package/dist/pi-intercom/broker/client-last.d.ts +13 -0
- package/dist/pi-intercom/broker/client-last.js +17 -5
- package/dist/pi-intercom/broker/client.d.ts +60 -0
- package/dist/pi-intercom/broker/client.js +47 -16
- 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 +26 -0
- package/dist/pi-intercom/broker/last.js +57 -32
- 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 +19 -0
- 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 +10 -2
- 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 +12 -38
- package/dist/pi-intercom/index.js +3 -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/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 +2 -0
- package/docs/development.md +40 -1
- package/docs/intercom-bridge.md +9 -4
- package/docs/intercom.md +226 -6
- package/docs/performance.md +87 -11
- package/docs/reference.md +157 -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/contracts.ts +8 -34
- package/src/agent/host.ts +19 -20
- package/src/agent/index.ts +224 -190
- 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 +405 -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/process.ts +240 -34
- package/src/agent/receipts.ts +114 -24
- package/src/agent/runner.ts +125 -27
- package/src/agent/shell.ts +2 -1
- package/src/agent/storage.ts +27 -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 +27 -23
- package/src/pi-intercom/broker/client-last.ts +25 -12
- package/src/pi-intercom/broker/client.ts +55 -33
- 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 +86 -33
- 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 +25 -0
- package/src/pi-intercom/connection.ts +21 -10
- 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 +18 -49
- package/src/pi-intercom/index.ts +3 -0
- package/src/pi-intercom/last-message.ts +10 -19
- 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/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/dist/agent/shell.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
-
import { readChildProcessIdentity } from "../shared/post-exit-stdio-guard.js";
|
|
5
|
+
import { readChildProcessIdentity, readProcessClock } from "../shared/post-exit-stdio-guard.js";
|
|
6
6
|
import { read, write } from "./storage.js";
|
|
7
7
|
import { processSchema } from "./contracts.js";
|
|
8
8
|
import { verifyProcess } from "./process.js";
|
|
@@ -39,5 +39,6 @@ write(path.join(directory, "shells", token, `${randomUUID()}.json`), {
|
|
|
39
39
|
pid,
|
|
40
40
|
identity,
|
|
41
41
|
token,
|
|
42
|
+
clock: readProcessClock(),
|
|
42
43
|
}, true);
|
|
43
44
|
verifyOwner(directory, token);
|
package/dist/agent/storage.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { readJsonProjection } from "../shared/journal-frames.js";
|
|
5
5
|
import { Assert } from "./typebox.js";
|
|
6
6
|
import { launchSchema, successorSchema } from "./contracts.js";
|
|
7
|
+
import { hasErrorCode } from "../shared/unknown.js";
|
|
7
8
|
export function missing(error) {
|
|
8
9
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
9
10
|
}
|
|
@@ -19,31 +20,47 @@ export function read(file, schema, select = () => true) {
|
|
|
19
20
|
throw error;
|
|
20
21
|
}
|
|
21
22
|
Assert(schema, value);
|
|
22
|
-
// TypeBox's generic Static<T> is intentionally the asserted schema's type.
|
|
23
|
-
// oxlint-disable-next-line typescript/no-unsafe-return
|
|
24
23
|
return value;
|
|
25
24
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const temp = `${file}.${randomUUID()}.tmp`;
|
|
29
|
-
const fd = fs.openSync(temp, "wx", 0o600);
|
|
25
|
+
/** Publish complete bytes without replacing a winner, even where Android denies hard links. */
|
|
26
|
+
function publishExclusive(temp, file) {
|
|
30
27
|
try {
|
|
31
|
-
fs.
|
|
32
|
-
|
|
28
|
+
fs.linkSync(temp, file);
|
|
29
|
+
return false;
|
|
33
30
|
}
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!["EACCES", "EPERM", "ENOTSUP"].some((code) => hasErrorCode(error, code))) {
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
// The immutable, private backing file must outlive its atomic no-clobber symlink.
|
|
36
|
+
fs.symlinkSync(path.basename(temp), file);
|
|
37
|
+
return true;
|
|
36
38
|
}
|
|
39
|
+
}
|
|
40
|
+
export function write(file, value, exclusive = false) {
|
|
41
|
+
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
42
|
+
const temp = `${file}.${randomUUID()}.${exclusive ? "data" : "tmp"}`;
|
|
43
|
+
const fd = fs.openSync(temp, "wx", 0o600);
|
|
44
|
+
let retained = false;
|
|
37
45
|
try {
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(fd, `${JSON.stringify(value)}\n`);
|
|
48
|
+
fs.fsyncSync(fd);
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
fs.closeSync(fd);
|
|
52
|
+
}
|
|
38
53
|
if (exclusive) {
|
|
39
|
-
|
|
54
|
+
retained = publishExclusive(temp, file);
|
|
40
55
|
}
|
|
41
56
|
else {
|
|
42
57
|
fs.renameSync(temp, file);
|
|
43
58
|
}
|
|
44
59
|
}
|
|
45
60
|
finally {
|
|
46
|
-
|
|
61
|
+
if (!retained) {
|
|
62
|
+
fs.rmSync(temp, { force: true });
|
|
63
|
+
}
|
|
47
64
|
}
|
|
48
65
|
}
|
|
49
66
|
export function safeId(id) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import { Type } from "./typebox.js";
|
|
3
|
+
// Main-session tool parameters only; the detached runner must not load pi-ai values.
|
|
4
|
+
export const delegateSchema = Type.Object({
|
|
5
|
+
prompt: Type.String({ minLength: 1 }),
|
|
6
|
+
name: Type.Optional(Type.String({ minLength: 1 })),
|
|
7
|
+
cwd: Type.Optional(Type.String({ minLength: 1 })),
|
|
8
|
+
model: Type.Optional(Type.String({
|
|
9
|
+
minLength: 1,
|
|
10
|
+
description: "Exact provider/model ID (:thinking and any case need host support); no fuzzy match or fallback.",
|
|
11
|
+
})),
|
|
12
|
+
context: Type.Optional(StringEnum(["fresh", "fork"])),
|
|
13
|
+
}, { additionalProperties: false });
|
|
14
|
+
export const runsSchema = Type.Object({
|
|
15
|
+
action: StringEnum(["list", "inspect", "continue", "stop"]),
|
|
16
|
+
id: Type.Optional(Type.String()),
|
|
17
|
+
prompt: Type.Optional(Type.String({ minLength: 1 })),
|
|
18
|
+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
19
|
+
length: Type.Optional(Type.Integer({ minimum: 4, maximum: 262144 })),
|
|
20
|
+
}, { additionalProperties: false });
|
package/dist/agent/typebox.js
CHANGED
|
@@ -9,12 +9,17 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { createRequire } from "node:module";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { pathToFileURL } from "node:url";
|
|
12
|
-
import { packageRoot } from "./host.js";
|
|
13
|
-
|
|
14
|
-
//
|
|
12
|
+
import { isChild, packageRoot } from "./host.js";
|
|
13
|
+
// Pi aliases these imports to its own instance (including bundled virtual modules).
|
|
14
|
+
// Only the detached runner needs absolute imports from the selected host package.
|
|
15
|
+
const require = isChild() ? createRequire(path.join(packageRoot(), "dist/index.js")) : undefined;
|
|
15
16
|
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
16
|
-
const typebox =
|
|
17
|
+
const typebox = require === undefined
|
|
18
|
+
? await import("typebox")
|
|
19
|
+
: await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox")).href));
|
|
17
20
|
// oxlint-disable-next-line typescript/no-unsafe-assignment
|
|
18
|
-
const value =
|
|
21
|
+
const value = require === undefined
|
|
22
|
+
? await import("typebox/value")
|
|
23
|
+
: await import(__rewriteRelativeImportExtension(pathToFileURL(require.resolve("typebox/value")).href));
|
|
19
24
|
export const { Type } = typebox;
|
|
20
25
|
export const Assert = value.Assert;
|
package/dist/agent/usage.js
CHANGED
|
@@ -1,127 +1,12 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { journalStamp } from "../shared/native-journal.js";
|
|
4
|
-
import { isDeepStrictEqual } from "node:util";
|
|
5
4
|
import { nativeProjection } from "../shared/journal-projections.js";
|
|
6
5
|
import { scanJournal } from "../shared/journal-frames.js";
|
|
7
6
|
import { isRecord } from "../shared/unknown.js";
|
|
8
7
|
import { Assert } from "./typebox.js";
|
|
9
8
|
import { contributionsSchema, resultSchema, } from "./contracts.js";
|
|
10
9
|
import { read, runDirectory, write } from "./storage.js";
|
|
11
|
-
export function sumUsage(values) {
|
|
12
|
-
const total = {
|
|
13
|
-
input: 0,
|
|
14
|
-
output: 0,
|
|
15
|
-
cacheRead: 0,
|
|
16
|
-
cacheWrite: 0,
|
|
17
|
-
totalTokens: 0,
|
|
18
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
19
|
-
};
|
|
20
|
-
for (const { usage } of values) {
|
|
21
|
-
for (const key of ["input", "output", "cacheRead", "cacheWrite", "totalTokens"]) {
|
|
22
|
-
total[key] += usage[key];
|
|
23
|
-
}
|
|
24
|
-
for (const key of ["input", "output", "cacheRead", "cacheWrite", "total"]) {
|
|
25
|
-
total.cost[key] += usage.cost[key];
|
|
26
|
-
}
|
|
27
|
-
for (const key of ["reasoning", "cacheWrite1h"]) {
|
|
28
|
-
if (usage[key] !== undefined) {
|
|
29
|
-
total[key] = (total[key] ?? 0) + usage[key];
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return total;
|
|
34
|
-
}
|
|
35
|
-
function intents(details) {
|
|
36
|
-
if (!isRecord(details) || details.agentUsage === undefined) {
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
const values = details.agentUsage;
|
|
40
|
-
Assert(contributionsSchema, values);
|
|
41
|
-
return values;
|
|
42
|
-
}
|
|
43
|
-
function publishedContributions(entry) {
|
|
44
|
-
if (!isRecord(entry) || entry.type !== "message" || !isRecord(entry.message)) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
const message = entry.message;
|
|
48
|
-
if (message.role !== "toolResult" ||
|
|
49
|
-
(message.toolName !== "delegate" && message.toolName !== "agent_runs")) {
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
const values = intents(message.details);
|
|
53
|
-
if (values === undefined || !isDeepStrictEqual(sumUsage(values), message.usage)) {
|
|
54
|
-
return [];
|
|
55
|
-
}
|
|
56
|
-
return values;
|
|
57
|
-
}
|
|
58
|
-
const receiptProjection = (keys) => {
|
|
59
|
-
if (keys.length === 0 || keys[0] === "type") {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
if (keys[0] !== "message") {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
if (keys.length === 1 || ["role", "toolName", "usage"].includes(String(keys[1]))) {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
return keys[1] === "details" && (keys.length === 2 || keys[2] === "agentUsage");
|
|
69
|
-
};
|
|
70
|
-
function priorUsage(ctx) {
|
|
71
|
-
const values = new Map();
|
|
72
|
-
const file = ctx.sessionManager.getSessionFile();
|
|
73
|
-
if (file !== undefined) {
|
|
74
|
-
scanJournal(file, receiptProjection, ({ value: entry }) => {
|
|
75
|
-
for (const value of publishedContributions(entry)) {
|
|
76
|
-
values.set(value.id, value);
|
|
77
|
-
}
|
|
78
|
-
}, { policy: "live", requireNewline: true });
|
|
79
|
-
}
|
|
80
|
-
for (const entry of ctx.sessionManager.getEntries()) {
|
|
81
|
-
for (const value of publishedContributions(entry)) {
|
|
82
|
-
values.set(value.id, value);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return values;
|
|
86
|
-
}
|
|
87
|
-
function uncharged(values, prior) {
|
|
88
|
-
const pending = [];
|
|
89
|
-
for (const value of values) {
|
|
90
|
-
const earlier = prior.get(value.id);
|
|
91
|
-
if (earlier && !isDeepStrictEqual(earlier, value)) {
|
|
92
|
-
throw new Error(`Conflicting native usage contribution ${value.id}`);
|
|
93
|
-
}
|
|
94
|
-
if (!earlier) {
|
|
95
|
-
pending.push(value);
|
|
96
|
-
prior.set(value.id, value);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return pending;
|
|
100
|
-
}
|
|
101
|
-
export function registerUsage(pi) {
|
|
102
|
-
pi.on("message_end", (event, ctx) => {
|
|
103
|
-
const message = event.message;
|
|
104
|
-
if (message.role !== "toolResult" ||
|
|
105
|
-
(message.toolName !== "delegate" && message.toolName !== "agent_runs")) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const values = intents(message.details);
|
|
109
|
-
if (values === undefined || !isRecord(message.details)) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const pending = uncharged(values, priorUsage(ctx));
|
|
113
|
-
const { usage: _usage, ...rest } = message;
|
|
114
|
-
// Finalized native tool messages are serialized even for concurrent tools.
|
|
115
|
-
// Preparing or discarding a result cannot reserve a charge.
|
|
116
|
-
return {
|
|
117
|
-
message: {
|
|
118
|
-
...rest,
|
|
119
|
-
details: { ...message.details, agentUsage: pending },
|
|
120
|
-
...(pending.length > 0 ? { usage: sumUsage(pending) } : {}),
|
|
121
|
-
},
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
10
|
function billing(entry) {
|
|
126
11
|
if (entry.checkpoint === true) {
|
|
127
12
|
return;
|
package/dist/agent/view.js
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { readOutputPage } from "../shared/output-page.js";
|
|
4
|
-
import { actor, alive } from "./process.js";
|
|
4
|
+
import { actor, alive, observeAlive } from "./process.js";
|
|
5
5
|
import { admissionSchema, commandSchema, exitSchema, successorSchema, } from "./contracts.js";
|
|
6
6
|
import { conversation, read, runDirectory } from "./storage.js";
|
|
7
7
|
import { published } from "./notifications.js";
|
|
8
8
|
import { readCanonicalResult, readResult } from "./usage.js";
|
|
9
9
|
import { Type } from "./typebox.js";
|
|
10
10
|
const commandMetadataSchema = Type.Pick(commandSchema, ["id", "runId", "action", "createdAt"]);
|
|
11
|
-
|
|
11
|
+
function toolResult(value, text) {
|
|
12
12
|
return { content: [{ type: "text", text }], details: value };
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function receiptResult(result) {
|
|
15
|
+
return result === undefined
|
|
16
|
+
? undefined
|
|
17
|
+
: {
|
|
18
|
+
id: result.id,
|
|
19
|
+
owner: result.owner,
|
|
20
|
+
status: result.status,
|
|
21
|
+
endedAt: result.endedAt,
|
|
22
|
+
terminalEntryId: result.terminalEntryId,
|
|
23
|
+
error: result.error,
|
|
24
|
+
accountingError: result.accountingError,
|
|
25
|
+
accountingPending: result.accountingPending,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** `observed` holds batched liveness by actor token; any other actor gets a fresh check. */
|
|
29
|
+
function processView(base, launch, observed) {
|
|
15
30
|
const identity = actor(base, launch);
|
|
16
31
|
const exit = identity
|
|
17
32
|
? read(path.join(conversation(base, launch), `exit-${identity.token}.json`), exitSchema)
|
|
18
33
|
: undefined;
|
|
19
34
|
return {
|
|
20
|
-
live: identity !== undefined && alive(identity),
|
|
35
|
+
live: identity !== undefined && (observed?.get(identity.token) ?? alive(identity)),
|
|
21
36
|
exit: exit ?? null,
|
|
22
37
|
exitKnown: exit !== undefined,
|
|
23
38
|
};
|
|
@@ -60,7 +75,7 @@ function guidanceView(base, launches) {
|
|
|
60
75
|
}
|
|
61
76
|
function metadata(base, launch, result, options) {
|
|
62
77
|
const directory = runDirectory(base, launch.id);
|
|
63
|
-
const process = processView(base, launch);
|
|
78
|
+
const process = processView(base, launch, options.observed);
|
|
64
79
|
const outputFile = path.join(directory, "output.txt");
|
|
65
80
|
const admission = read(path.join(conversation(base, launch), "admissions", `${launch.id}.json`), admissionSchema);
|
|
66
81
|
const status = result?.status ??
|
|
@@ -89,12 +104,18 @@ function metadata(base, launch, result, options) {
|
|
|
89
104
|
guidance: options.guidance,
|
|
90
105
|
};
|
|
91
106
|
}
|
|
92
|
-
|
|
107
|
+
async function summaries(base, launches, ctx) {
|
|
93
108
|
const guidance = guidanceView(base, launches);
|
|
109
|
+
const identities = [
|
|
110
|
+
...new Map(launches.map((launch) => [launch.rootId, actor(base, launch)])).values(),
|
|
111
|
+
].filter((identity) => identity !== undefined);
|
|
112
|
+
const verdicts = await observeAlive(identities);
|
|
113
|
+
const observed = new Map(identities.map((identity, index) => [identity.token, verdicts[index]]));
|
|
94
114
|
return launches.map((launch) => {
|
|
95
115
|
const view = metadata(base, launch, readCanonicalResult(base, launch), {
|
|
96
116
|
ctx,
|
|
97
117
|
guidance: guidance.get(launch.id) ?? [],
|
|
118
|
+
observed,
|
|
98
119
|
});
|
|
99
120
|
return {
|
|
100
121
|
...view,
|
|
@@ -109,7 +130,7 @@ export function summaries(base, launches, ctx) {
|
|
|
109
130
|
};
|
|
110
131
|
});
|
|
111
132
|
}
|
|
112
|
-
|
|
133
|
+
function projection(base, launch, ctx, input = {}) {
|
|
113
134
|
const view = metadata(base, launch, readResult(base, launch), {
|
|
114
135
|
ctx,
|
|
115
136
|
guidance: guidanceView(base, [launch]).get(launch.id) ?? [],
|
|
@@ -119,6 +140,22 @@ export function projection(base, launch, ctx, input = {}) {
|
|
|
119
140
|
output: outputView(view.outputFile, input),
|
|
120
141
|
};
|
|
121
142
|
}
|
|
143
|
+
export function renderReceipt(base, launch, ctx, receipt) {
|
|
144
|
+
const details = { ...projection(base, launch, ctx), ...receipt };
|
|
145
|
+
const model = { ...details, result: receiptResult(details.result) };
|
|
146
|
+
return toolResult(details, JSON.stringify(model, null, 2));
|
|
147
|
+
}
|
|
148
|
+
export async function renderInventory(base, launches, ctx, input) {
|
|
149
|
+
const offset = input.offset ?? 0;
|
|
150
|
+
const runs = await summaries(base, launches.slice(offset, offset + 50), ctx);
|
|
151
|
+
const details = {
|
|
152
|
+
runs,
|
|
153
|
+
offset,
|
|
154
|
+
nextOffset: offset + runs.length < launches.length ? offset + runs.length : undefined,
|
|
155
|
+
total: launches.length,
|
|
156
|
+
};
|
|
157
|
+
return toolResult(details, JSON.stringify(details, null, 2));
|
|
158
|
+
}
|
|
122
159
|
function receiptText(details) {
|
|
123
160
|
return details.nativeReceipt !== null
|
|
124
161
|
? `Native completion receipt: published (${details.nativeReceipt}); publication does not prove model consumption.`
|
|
@@ -148,15 +185,8 @@ function resultText(details) {
|
|
|
148
185
|
`Native journal: ${details.sessionFile}`,
|
|
149
186
|
].join("\n");
|
|
150
187
|
}
|
|
151
|
-
export function
|
|
152
|
-
const
|
|
153
|
-
const details = {
|
|
154
|
-
...view,
|
|
155
|
-
receivedResult: input.receivedResult === true,
|
|
156
|
-
...(input.receivedResult === true && view.result?.contributions !== undefined
|
|
157
|
-
? { agentUsage: view.result.contributions }
|
|
158
|
-
: {}),
|
|
159
|
-
};
|
|
188
|
+
export function renderInspection(base, launch, ctx, input = {}) {
|
|
189
|
+
const details = projection(base, launch, ctx, input);
|
|
160
190
|
return {
|
|
161
191
|
...toolResult(details, resultText(details)),
|
|
162
192
|
isError: details.result?.status === "failed",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { journalStamp } from "../shared/native-journal.js";
|
|
3
|
+
import { missing } from "./storage.js";
|
|
4
|
+
/** Identity and change stamp of a path; "" when absent. */
|
|
5
|
+
export function pathStamp(file) {
|
|
6
|
+
try {
|
|
7
|
+
const stat = fs.statSync(file, { bigint: true, throwIfNoEntry: false });
|
|
8
|
+
return stat === undefined ? "" : journalStamp(stat);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return "unreadable";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Native change hints for one directory identity; owners still reconcile canonical files. */
|
|
15
|
+
export class DirectoryWatch {
|
|
16
|
+
watcher;
|
|
17
|
+
armed = "";
|
|
18
|
+
checked = "";
|
|
19
|
+
failed = false;
|
|
20
|
+
directory;
|
|
21
|
+
relevant;
|
|
22
|
+
wake;
|
|
23
|
+
constructor(directory, relevant, wake) {
|
|
24
|
+
this.directory = directory;
|
|
25
|
+
this.relevant = relevant;
|
|
26
|
+
this.wake = wake;
|
|
27
|
+
}
|
|
28
|
+
ensure() {
|
|
29
|
+
const stat = fs.statSync(this.directory, { bigint: true, throwIfNoEntry: false });
|
|
30
|
+
const identity = stat?.isDirectory() === true ? `${stat.dev}:${stat.ino}` : "";
|
|
31
|
+
const checked = this.checked;
|
|
32
|
+
this.checked = stat === undefined ? "" : `${identity}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
33
|
+
if (this.watcher !== undefined && identity === this.armed) {
|
|
34
|
+
return checked === this.checked ? "watching" : "changed";
|
|
35
|
+
}
|
|
36
|
+
// A replaced directory leaves an inode watch silent on Linux.
|
|
37
|
+
this.close();
|
|
38
|
+
return identity === "" || this.failed ? "unwatched" : this.arm(identity);
|
|
39
|
+
}
|
|
40
|
+
arm(identity) {
|
|
41
|
+
try {
|
|
42
|
+
const watcher = fs.watch(this.directory, { persistent: false }, (_event, file) => {
|
|
43
|
+
if (watcher === this.watcher && this.relevant(file)) {
|
|
44
|
+
this.wake();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
watcher.on("error", () => {
|
|
48
|
+
if (watcher === this.watcher) {
|
|
49
|
+
this.failed = true;
|
|
50
|
+
this.close();
|
|
51
|
+
this.wake();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
this.watcher = watcher;
|
|
55
|
+
this.armed = identity;
|
|
56
|
+
return "armed";
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Unsupported platforms and watch limits keep their owner's polling fallback.
|
|
60
|
+
this.failed = !missing(error);
|
|
61
|
+
return "unwatched";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
close() {
|
|
65
|
+
this.watcher?.close();
|
|
66
|
+
this.watcher = undefined;
|
|
67
|
+
this.armed = "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** A coalesced wake for one owner loop; raised until that loop takes it. */
|
|
71
|
+
export class Wake {
|
|
72
|
+
raised = true;
|
|
73
|
+
resolve;
|
|
74
|
+
raise = () => {
|
|
75
|
+
this.raised = true;
|
|
76
|
+
this.resolve?.();
|
|
77
|
+
};
|
|
78
|
+
take() {
|
|
79
|
+
const raised = this.raised;
|
|
80
|
+
this.raised = false;
|
|
81
|
+
return raised;
|
|
82
|
+
}
|
|
83
|
+
/** Returns at once for a wake raised since the last take. */
|
|
84
|
+
async sleep(ms) {
|
|
85
|
+
if (this.raised) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
resolve("elapsed");
|
|
91
|
+
}, ms);
|
|
92
|
+
this.resolve = () => {
|
|
93
|
+
resolve("woken");
|
|
94
|
+
};
|
|
95
|
+
await promise;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
this.resolve = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
package/dist/build-info.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const EXTENSION_BUILD = Object.freeze({"version":"0.
|
|
1
|
+
export const EXTENSION_BUILD = Object.freeze({"version":"0.51.0","sha256":"fc084c36d416e6ea9947600b7d1fdef98c84a82f3408dea2ea028a9c485ac31e"});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type IntercomClientOptions as BrokerOptions } from "./pi-intercom/broker/client.ts";
|
|
2
|
+
import { type DeliveryGuard, type LastMessageQuery, type LastMessageResult, type ReceiptQuery, type ReceiptResult, type SendOptions, type SendResult, type SessionInfo } from "./pi-intercom/types.ts";
|
|
3
|
+
export type { ContextTarget, DeliveryGuard, LastMessageQuery, LastMessageResult, MessageDelivery, NativeReceipt, PublicationReplyHint, ReceiptQuery, ReceiptResult, SendResult, SessionInfo, } from "./pi-intercom/types.ts";
|
|
4
|
+
export type GuardedSendOptions = Omit<SendOptions, "messageId" | "guard" | "expectsReply" | "replyTo" | "human" | "topic" | "passive" | "delivery"> & {
|
|
5
|
+
readonly messageId: string;
|
|
6
|
+
readonly guard: DeliveryGuard;
|
|
7
|
+
readonly expectsReply: false;
|
|
8
|
+
readonly delivery: "queue" | "steer" | "passive";
|
|
9
|
+
};
|
|
10
|
+
export interface IntercomClientOptions extends BrokerOptions {
|
|
11
|
+
readonly spawn: false;
|
|
12
|
+
readonly session: Omit<SessionInfo, "id" | "guardedDelivery" | "applicationGuard" | "topics" | "subscriptions">;
|
|
13
|
+
readonly requestedId?: string;
|
|
14
|
+
readonly onError?: (error: Readonly<Error>) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface IntercomClient {
|
|
17
|
+
readonly sessionId: string | null;
|
|
18
|
+
readonly isConnected: () => boolean;
|
|
19
|
+
readonly listSessions: () => Promise<readonly SessionInfo[]>;
|
|
20
|
+
readonly send: (to: string, options: GuardedSendOptions) => Promise<SendResult>;
|
|
21
|
+
readonly last: (to: string, query?: LastMessageQuery) => Promise<LastMessageResult>;
|
|
22
|
+
readonly receipt: (to: string, query: ReceiptQuery) => Promise<ReceiptResult>;
|
|
23
|
+
readonly disconnect: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/** Connect to an existing native broker. No Pi runtime, broker spawn or automatic retry. */
|
|
26
|
+
export declare function connectIntercom(options: IntercomClientOptions): Promise<IntercomClient>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { IntercomClient as BrokerClient, } from "./pi-intercom/broker/client.js";
|
|
2
|
+
import { isDeliveryGuard, } from "./pi-intercom/types.js";
|
|
3
|
+
import { isRecord } from "./shared/unknown.js";
|
|
4
|
+
function requireNoSpawn(value) {
|
|
5
|
+
if (value !== false) {
|
|
6
|
+
throw new Error("The supported Intercom client requires spawn:false.");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function guardedSend(to, input) {
|
|
10
|
+
return (isRecord(input) &&
|
|
11
|
+
isDeliveryGuard(input.guard) &&
|
|
12
|
+
to === input.guard.peerId &&
|
|
13
|
+
typeof input.messageId === "string" &&
|
|
14
|
+
input.expectsReply === false &&
|
|
15
|
+
input.delivery !== undefined);
|
|
16
|
+
}
|
|
17
|
+
/** Connect to an existing native broker. No Pi runtime, broker spawn or automatic retry. */
|
|
18
|
+
export async function connectIntercom(options) {
|
|
19
|
+
requireNoSpawn(options.spawn);
|
|
20
|
+
const client = new BrokerClient(options);
|
|
21
|
+
client.on("error", (error) => options.onError?.(error));
|
|
22
|
+
try {
|
|
23
|
+
await client.connect(options.session, options.requestedId);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
await client.disconnect();
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
get sessionId() {
|
|
31
|
+
return client.sessionId;
|
|
32
|
+
},
|
|
33
|
+
isConnected: () => client.isConnected(),
|
|
34
|
+
listSessions: () => client.listSessions(),
|
|
35
|
+
async send(to, input) {
|
|
36
|
+
if (!guardedSend(to, input)) {
|
|
37
|
+
throw new Error("An exact guarded nonblocking send with an original message ID is required.");
|
|
38
|
+
}
|
|
39
|
+
return await client.send(to, input);
|
|
40
|
+
},
|
|
41
|
+
last: (to, query) => client.last(to, query),
|
|
42
|
+
receipt: (to, query) => client.receipt(to, query),
|
|
43
|
+
disconnect: () => client.disconnect(),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getPiAgentDir(): string;
|
|
@@ -4,6 +4,7 @@ import { IntercomClient } from "./broker/client.js";
|
|
|
4
4
|
import { resolveSessionProjectId } from "./session-targets.js";
|
|
5
5
|
import { Fault, cancelled, log, wait } from "./bridge-protocol.js";
|
|
6
6
|
import { Mailbox } from "./bridge-mailbox.js";
|
|
7
|
+
import { hasErrorCode } from "../shared/unknown.js";
|
|
7
8
|
import { resolveBridgeTarget, bridgeSessionList } from "./bridge-target.js";
|
|
8
9
|
const MAX_REQUESTS = 8;
|
|
9
10
|
const IDLE_TIMEOUT = 5 * 60 * 1000;
|
|
@@ -222,7 +223,7 @@ export class Session {
|
|
|
222
223
|
}
|
|
223
224
|
catch (error) {
|
|
224
225
|
cancelled(signal);
|
|
225
|
-
if (error
|
|
226
|
+
if (hasErrorCode(error, "list_timeout")) {
|
|
226
227
|
throw new Fault(504, "list_timeout", "Broker session list deadline exceeded.");
|
|
227
228
|
}
|
|
228
229
|
throw new Fault(503, "broker_unavailable", "Broker session list failed.");
|