larkin 0.4.18 → 0.4.20
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/dist/agent/context-prompt.cjs +2 -2
- package/dist/agent/context-prompt.mjs +2 -2
- package/dist/runtime/pi-inline-extensions.mjs +44 -8
- package/dist/runtime/pi-subagent-injection.mjs +103 -10
- package/dist/runtime/pi-subagent-ledger.cjs +632 -0
- package/dist/runtime/pi-subagent-ledger.mjs +594 -0
- package/dist/runtime/pi-subagent-record-watchdog-injection.mjs +43 -0
- package/dist/runtime/pi-subagent-record-watchdog.bundle.js +183 -0
- package/dist/runtime/pi-subagent-record-watchdog.mjs +46 -0
- package/dist/runtime/pi-subagents-notification.mjs +14 -0
- package/dist/runtime/pi-subagents.bundle.js +131 -58
- package/dist/runtime/pi-supervised-command-extension.mjs +72 -0
- package/dist/runtime/pi-supervised-command.bundle.js +4735 -0
- package/dist/runtime/pi-supervised-command.mjs +420 -0
- package/dist/runtime/runtime-adapters.mjs +52 -10
- package/dist/runtime/runtime-host.cjs +187 -14
- package/dist/runtime/runtime-host.mjs +187 -14
- package/package.json +1 -1
- package/patches/@tintinweb%2Fpi-subagents@0.14.3.patch +712 -16
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/runtime/pi-subagent-ledger.ts
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
var PI_SUBAGENT_RECORD_DIRNAME = "pi-subagent-records";
|
|
6
|
+
var PI_SUBAGENT_SESSION_OWNER_ENV = "LARKIN_PI_SESSION_OWNER";
|
|
7
|
+
var TERMINAL_STATUSES = new Set([
|
|
8
|
+
"completed",
|
|
9
|
+
"failed",
|
|
10
|
+
"cancelled",
|
|
11
|
+
"timed_out",
|
|
12
|
+
"orphaned"
|
|
13
|
+
]);
|
|
14
|
+
function isTerminalPiSubagentRecord(record) {
|
|
15
|
+
if (!record || typeof record !== "object")
|
|
16
|
+
return false;
|
|
17
|
+
const status = record.status;
|
|
18
|
+
return typeof status === "string" && status.length > 0 && status !== "running" && status !== "queued";
|
|
19
|
+
}
|
|
20
|
+
function isConsumedTerminalPiSubagentRecord(record) {
|
|
21
|
+
if (!isTerminalPiSubagentRecord(record))
|
|
22
|
+
return false;
|
|
23
|
+
return record.resultConsumed === true;
|
|
24
|
+
}
|
|
25
|
+
function ledgerStatusFromPiSubagentRecord(record) {
|
|
26
|
+
const status = typeof record === "object" && record && "status" in record ? String(record.status || "").trim().toLowerCase() : "";
|
|
27
|
+
if (status === "error")
|
|
28
|
+
return "failed";
|
|
29
|
+
if (status === "aborted")
|
|
30
|
+
return "timed_out";
|
|
31
|
+
if (status === "stopped")
|
|
32
|
+
return "cancelled";
|
|
33
|
+
if (status === "steered")
|
|
34
|
+
return "timed_out";
|
|
35
|
+
return "completed";
|
|
36
|
+
}
|
|
37
|
+
function readPersistedPiSubagentTerminal(file) {
|
|
38
|
+
return readPiSubagentTerminalFile(file, false);
|
|
39
|
+
}
|
|
40
|
+
function writePersistedPiSubagentTerminal(file, input, options) {
|
|
41
|
+
writePiSubagentTerminalFile(file, input, options?.resultConsumed === true);
|
|
42
|
+
}
|
|
43
|
+
function dispatchedSubagentRecordDir(stateDir) {
|
|
44
|
+
return path.join(stateDir, PI_SUBAGENT_RECORD_DIRNAME);
|
|
45
|
+
}
|
|
46
|
+
function sweepAbsentPiSubagentRecordFiles(recordDir, getRecord, options) {
|
|
47
|
+
let names;
|
|
48
|
+
try {
|
|
49
|
+
names = fs.readdirSync(recordDir);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code === "ENOENT")
|
|
52
|
+
return [];
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
const removed = [];
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
if (!name || name.startsWith("."))
|
|
58
|
+
continue;
|
|
59
|
+
const file = path.join(recordDir, name);
|
|
60
|
+
try {
|
|
61
|
+
const stat = fs.lstatSync(file);
|
|
62
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
63
|
+
continue;
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const owner = typeof options?.owner === "string" && options.owner.trim() ? options.owner.trim() : undefined;
|
|
68
|
+
if (owner && readPiSubagentRecordOwner(file) !== owner)
|
|
69
|
+
continue;
|
|
70
|
+
const record = getRecord(name);
|
|
71
|
+
if (isTerminalPiSubagentRecord(record)) {
|
|
72
|
+
try {
|
|
73
|
+
writePersistedPiSubagentTerminal(file, {
|
|
74
|
+
taskId: name,
|
|
75
|
+
status: ledgerStatusFromPiSubagentRecord(record)
|
|
76
|
+
}, { resultConsumed: isConsumedTerminalPiSubagentRecord(record) });
|
|
77
|
+
} catch {}
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (record != null)
|
|
81
|
+
continue;
|
|
82
|
+
if (readPersistedPiSubagentTerminal(file))
|
|
83
|
+
continue;
|
|
84
|
+
try {
|
|
85
|
+
fs.unlinkSync(file);
|
|
86
|
+
removed.push(name);
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
return removed;
|
|
90
|
+
}
|
|
91
|
+
function readPiSubagentTerminalFile(file, requireConsumed) {
|
|
92
|
+
if (!file)
|
|
93
|
+
return null;
|
|
94
|
+
try {
|
|
95
|
+
const stat = fs.lstatSync(file);
|
|
96
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
97
|
+
return null;
|
|
98
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
99
|
+
if (!parsed || typeof parsed !== "object")
|
|
100
|
+
return null;
|
|
101
|
+
if (requireConsumed && parsed.resultConsumed !== true && parsed.consumed !== true)
|
|
102
|
+
return null;
|
|
103
|
+
if (typeof parsed.taskId !== "string" || parsed.taskId.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
if (parsed.status !== "completed" && parsed.status !== "failed" && parsed.status !== "cancelled" && parsed.status !== "timed_out") {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
return { taskId: parsed.taskId, status: parsed.status };
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function writePiSubagentTerminalFile(file, input, resultConsumed) {
|
|
114
|
+
const existing = (() => {
|
|
115
|
+
try {
|
|
116
|
+
return fs.lstatSync(file);
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
if (existing?.isSymbolicLink())
|
|
122
|
+
throw new Error("subagent record file must not be a symlink");
|
|
123
|
+
const owner = readPiSubagentRecordOwner(file);
|
|
124
|
+
const payload = `${JSON.stringify({
|
|
125
|
+
taskId: input.taskId,
|
|
126
|
+
...resultConsumed ? { resultConsumed: true } : {},
|
|
127
|
+
status: input.status,
|
|
128
|
+
...owner ? { owner } : {}
|
|
129
|
+
})}
|
|
130
|
+
`;
|
|
131
|
+
if (existing?.isFile()) {
|
|
132
|
+
try {
|
|
133
|
+
if (fs.readFileSync(file, "utf8") === payload)
|
|
134
|
+
return;
|
|
135
|
+
} catch {}
|
|
136
|
+
}
|
|
137
|
+
fs.writeFileSync(file, payload, { mode: 384 });
|
|
138
|
+
fs.chmodSync(file, 384);
|
|
139
|
+
}
|
|
140
|
+
function readPiSubagentRecordOwner(file) {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
143
|
+
return typeof parsed?.owner === "string" && parsed.owner.trim() ? parsed.owner.trim() : undefined;
|
|
144
|
+
} catch {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/runtime/pi-subagent-record-watchdog.ts
|
|
150
|
+
var PI_SUBAGENTS_MANAGER = Symbol.for("pi-subagents:manager");
|
|
151
|
+
var DEFAULT_SWEEP_INTERVAL_MS = 15000;
|
|
152
|
+
function piSubagentRecordWatchdog(pi) {
|
|
153
|
+
const stateDir = process.env.LARKIN_STATE_DIR;
|
|
154
|
+
const owner = process.env[PI_SUBAGENT_SESSION_OWNER_ENV];
|
|
155
|
+
if (!stateDir || !owner)
|
|
156
|
+
return;
|
|
157
|
+
const recordDir = dispatchedSubagentRecordDir(stateDir);
|
|
158
|
+
const intervalMs = sweepIntervalMs();
|
|
159
|
+
const tick = () => {
|
|
160
|
+
try {
|
|
161
|
+
const manager = globalThis[PI_SUBAGENTS_MANAGER];
|
|
162
|
+
if (typeof manager?.getRecord !== "function")
|
|
163
|
+
return;
|
|
164
|
+
sweepAbsentPiSubagentRecordFiles(recordDir, (taskId) => manager.getRecord(taskId), { owner });
|
|
165
|
+
} catch {}
|
|
166
|
+
};
|
|
167
|
+
const timer = setInterval(tick, intervalMs);
|
|
168
|
+
timer.unref?.();
|
|
169
|
+
const stop = () => {
|
|
170
|
+
clearInterval(timer);
|
|
171
|
+
tick();
|
|
172
|
+
};
|
|
173
|
+
pi.on("session_shutdown", stop);
|
|
174
|
+
}
|
|
175
|
+
function sweepIntervalMs() {
|
|
176
|
+
const raw = Number.parseInt(process.env.LARKIN_PI_SUBAGENT_RECORD_SWEEP_MS || "", 10);
|
|
177
|
+
if (Number.isInteger(raw) && raw > 0)
|
|
178
|
+
return raw;
|
|
179
|
+
return DEFAULT_SWEEP_INTERVAL_MS;
|
|
180
|
+
}
|
|
181
|
+
export {
|
|
182
|
+
piSubagentRecordWatchdog as default
|
|
183
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { dispatchedSubagentRecordDir, PI_SUBAGENT_SESSION_OWNER_ENV, sweepAbsentPiSubagentRecordFiles, } from "./pi-subagent-ledger.mjs";
|
|
2
|
+
const PI_SUBAGENTS_MANAGER = Symbol.for("pi-subagents:manager");
|
|
3
|
+
const DEFAULT_SWEEP_INTERVAL_MS = 15_000;
|
|
4
|
+
/**
|
|
5
|
+
* Pi-process watchdog: delete Larkin record sidecars once AgentManager no longer
|
|
6
|
+
* has the in-memory task. Persist any terminal AgentRecord — including an
|
|
7
|
+
* unconsumed completion Pi may evict after ten minutes — before treating a
|
|
8
|
+
* missing getRecord as a genuine orphan. A get_subagent_result consume also
|
|
9
|
+
* suppresses the canonical notification; bridge that consumed marker before
|
|
10
|
+
* cleanup can delete the sidecar. Leftover transcripts are intentionally
|
|
11
|
+
* ignored. Sidecars are owner-tagged so a staged Pi sharing LARKIN_STATE_DIR
|
|
12
|
+
* cannot sweep another session's records. This extension registers
|
|
13
|
+
* session_shutdown first so the final sweep still sees getRecord.
|
|
14
|
+
*/
|
|
15
|
+
export default function piSubagentRecordWatchdog(pi) {
|
|
16
|
+
const stateDir = process.env.LARKIN_STATE_DIR;
|
|
17
|
+
const owner = process.env[PI_SUBAGENT_SESSION_OWNER_ENV];
|
|
18
|
+
if (!stateDir || !owner)
|
|
19
|
+
return;
|
|
20
|
+
const recordDir = dispatchedSubagentRecordDir(stateDir);
|
|
21
|
+
const intervalMs = sweepIntervalMs();
|
|
22
|
+
const tick = () => {
|
|
23
|
+
try {
|
|
24
|
+
const manager = globalThis[PI_SUBAGENTS_MANAGER];
|
|
25
|
+
if (typeof manager?.getRecord !== "function")
|
|
26
|
+
return;
|
|
27
|
+
sweepAbsentPiSubagentRecordFiles(recordDir, (taskId) => manager.getRecord(taskId), { owner });
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Best-effort: a transient EACCES/EMFILE must not crash the Pi timer.
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const timer = setInterval(tick, intervalMs);
|
|
34
|
+
timer.unref?.();
|
|
35
|
+
const stop = () => {
|
|
36
|
+
clearInterval(timer);
|
|
37
|
+
tick();
|
|
38
|
+
};
|
|
39
|
+
pi.on("session_shutdown", stop);
|
|
40
|
+
}
|
|
41
|
+
function sweepIntervalMs() {
|
|
42
|
+
const raw = Number.parseInt(process.env.LARKIN_PI_SUBAGENT_RECORD_SWEEP_MS || "", 10);
|
|
43
|
+
if (Number.isInteger(raw) && raw > 0)
|
|
44
|
+
return raw;
|
|
45
|
+
return DEFAULT_SWEEP_INTERVAL_MS;
|
|
46
|
+
}
|
|
@@ -20,6 +20,20 @@ const TERMINAL_STATUS_TOKENS = new Set([
|
|
|
20
20
|
"turn-limit",
|
|
21
21
|
"turn_limit",
|
|
22
22
|
]);
|
|
23
|
+
export function ledgerStatusFromPiNotificationStatus(status) {
|
|
24
|
+
const normalized = status.trim().toLowerCase();
|
|
25
|
+
if (normalized.startsWith("error") || normalized === "error")
|
|
26
|
+
return "failed";
|
|
27
|
+
if (normalized.startsWith("stopped"))
|
|
28
|
+
return "cancelled";
|
|
29
|
+
if (normalized.startsWith("aborted")
|
|
30
|
+
|| normalized.includes("turn limit")
|
|
31
|
+
|| normalized.includes("turn-limit")
|
|
32
|
+
|| normalized.includes("turn_limit")
|
|
33
|
+
|| normalized.includes("steered"))
|
|
34
|
+
return "timed_out";
|
|
35
|
+
return "completed";
|
|
36
|
+
}
|
|
23
37
|
export function isCanonicalTerminalPiSubagentStatus(status) {
|
|
24
38
|
const normalized = status.trim().toLowerCase();
|
|
25
39
|
if (!normalized)
|