@peakflo/agent-harness 0.0.0-stage → 0.2.2-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -2
  3. package/dist/adapters/acp-adapter.d.ts +177 -0
  4. package/dist/adapters/acp-adapter.js +1732 -0
  5. package/dist/adapters/claude-code-adapter.d.ts +171 -0
  6. package/dist/adapters/claude-code-adapter.js +1918 -0
  7. package/dist/adapters/codex-adapter.d.ts +67 -0
  8. package/dist/adapters/codex-adapter.js +681 -0
  9. package/dist/adapters/codex-app-server-adapter.d.ts +105 -0
  10. package/dist/adapters/codex-app-server-adapter.js +1309 -0
  11. package/dist/adapters/coding-agent-adapter.d.ts +289 -0
  12. package/dist/adapters/coding-agent-adapter.js +31 -0
  13. package/dist/adapters/index.d.ts +17 -0
  14. package/dist/adapters/index.js +35 -0
  15. package/dist/adapters/model-gateway.d.ts +84 -0
  16. package/dist/adapters/model-gateway.js +270 -0
  17. package/dist/adapters/opencode-adapter.d.ts +403 -0
  18. package/dist/adapters/opencode-adapter.js +2710 -0
  19. package/dist/adapters/opencode-config.d.ts +41 -0
  20. package/dist/adapters/opencode-config.js +147 -0
  21. package/dist/adapters/opencode-server-policy.d.ts +82 -0
  22. package/dist/adapters/opencode-server-policy.js +148 -0
  23. package/dist/adapters/pi-adapter.d.ts +168 -0
  24. package/dist/adapters/pi-adapter.js +1468 -0
  25. package/dist/capabilities.d.ts +58 -0
  26. package/dist/capabilities.js +161 -0
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +231 -0
  29. package/dist/contract.d.ts +273 -0
  30. package/dist/contract.js +357 -0
  31. package/dist/control-plane.d.ts +178 -0
  32. package/dist/control-plane.js +431 -0
  33. package/dist/credential-store.d.ts +110 -0
  34. package/dist/credential-store.js +181 -0
  35. package/dist/enrol/browser-enrol.d.ts +85 -0
  36. package/dist/enrol/browser-enrol.js +387 -0
  37. package/dist/github-command-guards.d.ts +20 -0
  38. package/dist/github-command-guards.js +330 -0
  39. package/dist/github-proxy-certificates.d.ts +25 -0
  40. package/dist/github-proxy-certificates.js +145 -0
  41. package/dist/github-proxy.d.ts +37 -0
  42. package/dist/github-proxy.js +476 -0
  43. package/dist/github-workspace.d.ts +55 -0
  44. package/dist/github-workspace.js +537 -0
  45. package/dist/harness.d.ts +333 -0
  46. package/dist/harness.js +1414 -0
  47. package/dist/health.d.ts +45 -0
  48. package/dist/health.js +116 -0
  49. package/dist/index.d.ts +53 -0
  50. package/dist/index.js +123 -0
  51. package/dist/model-probe.d.ts +96 -0
  52. package/dist/model-probe.js +195 -0
  53. package/dist/options.d.ts +15 -0
  54. package/dist/options.js +212 -0
  55. package/dist/ports-default.d.ts +212 -0
  56. package/dist/ports-default.js +333 -0
  57. package/dist/ports.d.ts +436 -0
  58. package/dist/ports.js +27 -0
  59. package/dist/runtime/attachments.d.ts +92 -0
  60. package/dist/runtime/attachments.js +377 -0
  61. package/dist/runtime/child-stream-guards.d.ts +45 -0
  62. package/dist/runtime/child-stream-guards.js +111 -0
  63. package/dist/runtime/conversation.d.ts +87 -0
  64. package/dist/runtime/conversation.js +156 -0
  65. package/dist/runtime/lease-renewal.d.ts +169 -0
  66. package/dist/runtime/lease-renewal.js +433 -0
  67. package/dist/runtime/mcp-auth-proxy.d.ts +82 -0
  68. package/dist/runtime/mcp-auth-proxy.js +300 -0
  69. package/dist/runtime/permission-mode.d.ts +116 -0
  70. package/dist/runtime/permission-mode.js +146 -0
  71. package/dist/runtime/projection.d.ts +71 -0
  72. package/dist/runtime/projection.js +211 -0
  73. package/dist/runtime/session-runner.d.ts +520 -0
  74. package/dist/runtime/session-runner.js +2122 -0
  75. package/dist/shared/reasoning-effort.d.ts +11 -0
  76. package/dist/shared/reasoning-effort.js +20 -0
  77. package/dist/store/conversation-store.d.ts +100 -0
  78. package/dist/store/conversation-store.js +305 -0
  79. package/dist/store/sqlite-store.d.ts +120 -0
  80. package/dist/store/sqlite-store.js +346 -0
  81. package/dist/types.d.ts +952 -0
  82. package/dist/types.js +55 -0
  83. package/dist/version.d.ts +32 -0
  84. package/dist/version.js +77 -0
  85. package/package.json +53 -4
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ /**
3
+ * THE HARNESS TRANSCRIPT STORE.
4
+ *
5
+ * Decision: KEEP THE SQLITE STORE. A runner that holds its working state only
6
+ * in memory loses a turn to its own crash, and the crash is exactly when the
7
+ * transcript matters. The control plane stays the source of truth via
8
+ * `POST /ingest`; this table is WORKING STATE — what has been produced, and
9
+ * what has been acknowledged — so a restarted harness can resume a lease
10
+ * instead of replaying a turn the user already paid for.
11
+ *
12
+ * WHY `node:sqlite` AND NOT `better-sqlite3`, which 20x uses:
13
+ *
14
+ * `pf-bootstrap` installs the harness with `--ignore-scripts` (a Phase 0
15
+ * hardening decision: an npm install script runs arbitrary code as the harness
16
+ * user before any of our code is reached). `better-sqlite3` is a NATIVE module
17
+ * whose binary is fetched or built by `prebuild-install` — an install script.
18
+ * Depending on it would therefore break the one boot path Phase 0 actually
19
+ * proved, and it would break it silently, at runtime, on a customer VM.
20
+ *
21
+ * `node:sqlite` ships INSIDE Node 22 (unflagged from 22.13). It costs no
22
+ * dependency, no native build, no install script and no prebuild matrix, and
23
+ * the sandbox image is already `node:22-bookworm-slim`. On an older Node the
24
+ * module is simply absent and `openTranscriptStore` falls back to memory, which
25
+ * costs crash-resumption and nothing else.
26
+ *
27
+ * THE SEVEN INVARIANTS are ported from 20x `src/main/database.ts` (schema at
28
+ * :1225, upsert at :2118, delta at :2179, snapshot at :2209) and each is
29
+ * asserted individually in the tests. They are NOT approximated: the control
30
+ * plane implements the same seven, and a transcript written by a runner must be
31
+ * byte-for-byte the transcript the desktop would have written.
32
+ */
33
+ Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.MemoryTranscriptStore = exports.SqliteTranscriptStore = void 0;
35
+ exports.openTranscriptStore = openTranscriptStore;
36
+ const SCHEMA = `
37
+ CREATE TABLE IF NOT EXISTS transcript_parts (
38
+ session_id TEXT NOT NULL,
39
+ part_id TEXT NOT NULL,
40
+ seq INTEGER NOT NULL,
41
+ role TEXT NOT NULL DEFAULT 'system',
42
+ content TEXT NOT NULL DEFAULT '',
43
+ part_type TEXT,
44
+ tool TEXT,
45
+ payload TEXT,
46
+ created_at INTEGER NOT NULL,
47
+ updated_at INTEGER NOT NULL,
48
+ -- Monotonic change cursor, bumped on every insert AND every content
49
+ -- update, so a reader can ask for "everything changed since rev N" and
50
+ -- catch both new parts and streaming edits to existing ones.
51
+ rev INTEGER NOT NULL DEFAULT 0,
52
+ PRIMARY KEY (session_id, part_id)
53
+ );
54
+ CREATE INDEX IF NOT EXISTS idx_tp_session_seq ON transcript_parts(session_id, seq);
55
+ CREATE INDEX IF NOT EXISTS idx_tp_session_rev ON transcript_parts(session_id, rev);
56
+ `;
57
+ function errorMessage(error) {
58
+ return error instanceof Error ? error.message : String(error);
59
+ }
60
+ function toStoredPart(row) {
61
+ return {
62
+ partId: row.part_id,
63
+ seq: row.seq,
64
+ role: row.role || 'system',
65
+ content: row.content,
66
+ partType: row.part_type ?? '',
67
+ tool: row.tool,
68
+ payload: row.payload ? JSON.parse(row.payload) : null,
69
+ createdAt: row.created_at,
70
+ updatedAt: row.updated_at,
71
+ rev: row.rev
72
+ };
73
+ }
74
+ /* -------------------------------------------------------------------------- */
75
+ /* SQLite implementation */
76
+ /* -------------------------------------------------------------------------- */
77
+ class SqliteTranscriptStore {
78
+ db;
79
+ constructor(db) {
80
+ this.db = db;
81
+ // WAL so a reader (a health probe, a debug dump) never blocks the agent
82
+ // loop's writes. A transcript is written on every streaming delta.
83
+ try {
84
+ this.db.exec('PRAGMA journal_mode = WAL;');
85
+ }
86
+ catch {
87
+ // A filesystem that cannot do WAL (some container overlays) still works
88
+ // in the default rollback journal. Not worth failing a boot over.
89
+ }
90
+ this.db.exec(SCHEMA);
91
+ }
92
+ /**
93
+ * INVARIANTS 1-5, in one transaction.
94
+ *
95
+ * The transaction is explicit BEGIN/COMMIT because `node:sqlite` has no
96
+ * `db.transaction()` wrapper of the kind better-sqlite3 provides. The
97
+ * rollback on failure is what makes invariant 1 true: a batch that fails
98
+ * half-way must leave no parts behind, or a retry would see a transcript that
99
+ * never existed.
100
+ */
101
+ upsertParts(sessionId, parts) {
102
+ if (parts.length === 0) {
103
+ return { maxRev: this.getMaxRev(sessionId), changedPartIds: [] };
104
+ }
105
+ const nextSeqStmt = this.db.prepare('SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM transcript_parts WHERE session_id = ?');
106
+ const globalRevStmt = this.db.prepare('SELECT COALESCE(MAX(rev), 0) AS m FROM transcript_parts');
107
+ // INVARIANT 2: `seq` is absent from the DO UPDATE list, so an existing part
108
+ // KEEPS the position it was first given. That is precisely how token
109
+ // streaming works — without it a growing text part would jump to the end of
110
+ // the transcript on every single delta.
111
+ //
112
+ // INVARIANT 3: part_type/tool/payload COALESCE against the STORED value, so
113
+ // a partial update (a streaming delta carries content and nothing else)
114
+ // cannot erase metadata that a fuller earlier update established.
115
+ //
116
+ // INVARIANT 4: created_at is likewise absent from the DO UPDATE list. The
117
+ // first write wins, for ever. A later reconcile pass must not restamp
118
+ // history with the time it happened to run.
119
+ const upsertStmt = this.db.prepare(`
120
+ INSERT INTO transcript_parts
121
+ (session_id, part_id, seq, role, content, part_type, tool, payload, created_at, updated_at, rev)
122
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
123
+ ON CONFLICT(session_id, part_id) DO UPDATE SET
124
+ content = excluded.content,
125
+ part_type = COALESCE(excluded.part_type, transcript_parts.part_type),
126
+ tool = COALESCE(excluded.tool, transcript_parts.tool),
127
+ payload = COALESCE(excluded.payload, transcript_parts.payload),
128
+ updated_at = excluded.updated_at,
129
+ rev = excluded.rev
130
+ `);
131
+ const changedPartIds = [];
132
+ let maxRev = 0;
133
+ this.db.exec('BEGIN');
134
+ try {
135
+ let nextSeq = nextSeqStmt.get(sessionId).next;
136
+ // INVARIANT 5: rev is allocated from the GLOBAL maximum and bumped for
137
+ // every changed row, inserts and updates alike.
138
+ let rev = globalRevStmt.get().m;
139
+ const writeNow = Date.now();
140
+ for (const part of parts) {
141
+ if (!part.partId) {
142
+ continue;
143
+ }
144
+ rev += 1;
145
+ upsertStmt.run(sessionId, part.partId, nextSeq, part.role || 'system', part.content || '', part.partType ?? null, part.tool ?? null,
146
+ // `undefined` and `null` both store as NULL, which COALESCE then
147
+ // reads as "leave the stored value alone". The contract's
148
+ // payload-clear semantics live one layer up, in `isPersistablePart`
149
+ // and the ingest path — SQL COALESCE cannot express "explicitly
150
+ // null" and pretending otherwise here would be a lie in the schema.
151
+ part.payload === undefined || part.payload === null
152
+ ? null
153
+ : JSON.stringify(part.payload),
154
+ // The part's ORIGINAL time when known, never the write time.
155
+ // Otherwise a bulk replay stamps the whole history within a
156
+ // millisecond of itself and destroys the transcript's chronology.
157
+ typeof part.createdAt === 'number' ? part.createdAt : writeNow, writeNow, rev);
158
+ // seq is consumed per part, matching 20x. A conflicting row ignores the
159
+ // value it was handed, so the counter simply skips — gaps in seq are
160
+ // expected and harmless, since seq is only ever a tiebreaker.
161
+ nextSeq += 1;
162
+ changedPartIds.push(part.partId);
163
+ }
164
+ maxRev = rev;
165
+ this.db.exec('COMMIT');
166
+ }
167
+ catch (error) {
168
+ try {
169
+ this.db.exec('ROLLBACK');
170
+ }
171
+ catch {
172
+ // Rolling back a transaction that already aborted is not an error.
173
+ }
174
+ throw error;
175
+ }
176
+ return { maxRev, changedPartIds };
177
+ }
178
+ /** INVARIANTS 6 and 7. */
179
+ getDelta(sessionId, sinceRev) {
180
+ const rows = this.db
181
+ .prepare(`SELECT * FROM transcript_parts
182
+ WHERE session_id = ? AND rev > ?
183
+ ORDER BY created_at ASC, seq ASC`)
184
+ .all(sessionId, sinceRev);
185
+ // INVARIANT 6: maxRev is PER SESSION. A global maximum would hand a client
186
+ // a cursor that another session had already advanced past, and the client
187
+ // would then never be told about its own next part.
188
+ const max = this.db
189
+ .prepare('SELECT COALESCE(MAX(rev), ?) AS m FROM transcript_parts WHERE session_id = ?')
190
+ .get(sinceRev, sessionId);
191
+ return { parts: rows.map(toStoredPart), maxRev: max.m };
192
+ }
193
+ getParts(sessionId) {
194
+ // INVARIANT 7: ordered by REAL event time, with seq as a stable tiebreaker.
195
+ // Insertion order is NOT chronological once a partial projection has older
196
+ // history backfilled into it.
197
+ const rows = this.db
198
+ .prepare(`SELECT * FROM transcript_parts
199
+ WHERE session_id = ?
200
+ ORDER BY created_at ASC, seq ASC`)
201
+ .all(sessionId);
202
+ return rows.map(toStoredPart);
203
+ }
204
+ getMaxRev(sessionId) {
205
+ const row = this.db
206
+ .prepare('SELECT COALESCE(MAX(rev), 0) AS m FROM transcript_parts WHERE session_id = ?')
207
+ .get(sessionId);
208
+ return row.m;
209
+ }
210
+ deleteParts(sessionId) {
211
+ this.db
212
+ .prepare('DELETE FROM transcript_parts WHERE session_id = ?')
213
+ .run(sessionId);
214
+ }
215
+ close() {
216
+ try {
217
+ this.db.close();
218
+ }
219
+ catch {
220
+ // Closing twice is not an error worth propagating out of a shutdown path.
221
+ }
222
+ }
223
+ }
224
+ exports.SqliteTranscriptStore = SqliteTranscriptStore;
225
+ /* -------------------------------------------------------------------------- */
226
+ /* In-memory fallback */
227
+ /* -------------------------------------------------------------------------- */
228
+ /**
229
+ * The same seven invariants, without durability.
230
+ *
231
+ * Used when `node:sqlite` is absent, and in tests. It is a REAL implementation
232
+ * rather than a stub: if the two disagreed, every test that used this would be
233
+ * testing something the product does not do. The tests run the same invariant
234
+ * suite against both.
235
+ */
236
+ class MemoryTranscriptStore {
237
+ bySession = new Map();
238
+ globalRev = 0;
239
+ upsertParts(sessionId, parts) {
240
+ let session = this.bySession.get(sessionId);
241
+ if (!session) {
242
+ session = new Map();
243
+ this.bySession.set(sessionId, session);
244
+ }
245
+ const changedPartIds = [];
246
+ const writeNow = Date.now();
247
+ let nextSeq = Math.max(0, ...Array.from(session.values(), (p) => p.seq)) + 1;
248
+ for (const part of parts) {
249
+ if (!part.partId) {
250
+ continue;
251
+ }
252
+ this.globalRev += 1;
253
+ const existing = session.get(part.partId);
254
+ if (existing) {
255
+ // Invariants 2, 3 and 4: keep seq, keep createdAt, coalesce metadata.
256
+ existing.content = part.content || '';
257
+ existing.partType = part.partType ?? existing.partType;
258
+ existing.tool = part.tool ?? existing.tool;
259
+ existing.payload =
260
+ part.payload === undefined || part.payload === null
261
+ ? existing.payload
262
+ : part.payload;
263
+ existing.updatedAt = writeNow;
264
+ existing.rev = this.globalRev;
265
+ }
266
+ else {
267
+ session.set(part.partId, {
268
+ partId: part.partId,
269
+ seq: nextSeq,
270
+ role: part.role || 'system',
271
+ content: part.content || '',
272
+ partType: part.partType ?? '',
273
+ tool: part.tool ?? null,
274
+ payload: part.payload ?? null,
275
+ createdAt: typeof part.createdAt === 'number' ? part.createdAt : writeNow,
276
+ updatedAt: writeNow,
277
+ rev: this.globalRev
278
+ });
279
+ }
280
+ nextSeq += 1;
281
+ changedPartIds.push(part.partId);
282
+ }
283
+ return { maxRev: this.globalRev, changedPartIds };
284
+ }
285
+ getDelta(sessionId, sinceRev) {
286
+ const parts = this.ordered(sessionId).filter((p) => p.rev > sinceRev);
287
+ const all = this.ordered(sessionId);
288
+ const maxRev = all.length ? Math.max(...all.map((p) => p.rev)) : sinceRev;
289
+ return { parts, maxRev };
290
+ }
291
+ getParts(sessionId) {
292
+ return this.ordered(sessionId);
293
+ }
294
+ getMaxRev(sessionId) {
295
+ const all = this.ordered(sessionId);
296
+ return all.length ? Math.max(...all.map((p) => p.rev)) : 0;
297
+ }
298
+ deleteParts(sessionId) {
299
+ this.bySession.delete(sessionId);
300
+ }
301
+ ordered(sessionId) {
302
+ const session = this.bySession.get(sessionId);
303
+ if (!session) {
304
+ return [];
305
+ }
306
+ return Array.from(session.values())
307
+ .map((p) => ({ ...p }))
308
+ .sort((a, b) => a.createdAt - b.createdAt || a.seq - b.seq);
309
+ }
310
+ }
311
+ exports.MemoryTranscriptStore = MemoryTranscriptStore;
312
+ /**
313
+ * Open the durable store, or say plainly why it could not be opened.
314
+ *
315
+ * NEVER THROWS. A harness that cannot open a database is still able to run a
316
+ * turn and push it to the control plane, which is the source of truth; refusing
317
+ * to boot would trade a recoverable degradation for an outage. The caller
318
+ * reports `durable: false` in its health snapshot so the degradation is VISIBLE
319
+ * rather than silent — a fallback nobody can see is how an outage becomes a
320
+ * mystery.
321
+ */
322
+ function openTranscriptStore(path) {
323
+ let sqlite;
324
+ try {
325
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, @typescript-eslint/no-require-imports
326
+ sqlite = require('node:sqlite');
327
+ }
328
+ catch (error) {
329
+ return {
330
+ store: new MemoryTranscriptStore(),
331
+ durable: false,
332
+ reason: `node:sqlite unavailable (needs Node >= 22.13): ${errorMessage(error)}`
333
+ };
334
+ }
335
+ try {
336
+ const db = new sqlite.DatabaseSync(path);
337
+ return { store: new SqliteTranscriptStore(db), durable: true };
338
+ }
339
+ catch (error) {
340
+ return {
341
+ store: new MemoryTranscriptStore(),
342
+ durable: false,
343
+ reason: `could not open ${path}: ${errorMessage(error)}`
344
+ };
345
+ }
346
+ }