@botbotgo/agent-harness 0.0.138 → 0.0.141

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/README.md CHANGED
@@ -135,10 +135,14 @@ Boundary documents live in:
135
135
  - `docs/acp-support-plan.md`
136
136
  - `docs/recovery-policy-matrix.md`
137
137
  - `docs/operator-timeline.md`
138
+ - `docs/runtime-inspection-contract.md`
138
139
  - `docs/queue-concurrency-inspection.md`
139
140
  - `docs/tool-execution-policy.md`
140
141
  - `docs/feature-checklist.md`
141
142
  - `docs/long-term-memory.md`
143
+ - `docs/memory-runtime-design.md`
144
+ - `docs/memory-runtime-usage.md`
145
+ - `docs/memory-policy-reference.md`
142
146
  - `docs/app-task-pattern.md`
143
147
  - `docs/model-layering.md`
144
148
  - `docs/coding-agent-guide.md`
package/README.zh.md CHANGED
@@ -135,6 +135,7 @@ AI 让 agent 逻辑、工具调用和工作流代码更容易生成,真正变
135
135
  - `docs/acp-support-plan.md`
136
136
  - `docs/recovery-policy-matrix.md`
137
137
  - `docs/operator-timeline.md`
138
+ - `docs/runtime-inspection-contract.md`
138
139
  - `docs/queue-concurrency-inspection.md`
139
140
  - `docs/tool-execution-policy.md`
140
141
  - `docs/feature-checklist.md`
package/dist/api.js CHANGED
@@ -10,6 +10,7 @@ function toSessionSummary(summary) {
10
10
  createdAt: summary.createdAt,
11
11
  updatedAt: summary.updatedAt,
12
12
  status: summary.status,
13
+ currentAgentId: summary.currentAgentId,
13
14
  };
14
15
  }
15
16
  function toRequestSummary(summary) {
@@ -24,12 +25,19 @@ function toRequestSummary(summary) {
24
25
  state: summary.state,
25
26
  checkpointRef: summary.checkpointRef,
26
27
  resumable: summary.resumable,
28
+ startedAt: summary.startedAt,
29
+ endedAt: summary.endedAt,
30
+ lastActivityAt: summary.lastActivityAt,
31
+ currentAgentId: summary.currentAgentId,
32
+ delegationChain: summary.delegationChain,
33
+ runtimeSnapshot: summary.runtimeSnapshot,
27
34
  };
28
35
  }
29
36
  function toSessionRecord(record) {
30
37
  return {
31
38
  sessionId: record.threadId,
32
39
  entryAgentId: record.entryAgentId,
40
+ currentAgentId: record.currentAgentId,
33
41
  currentState: record.currentState,
34
42
  latestRequestId: record.latestRunId,
35
43
  createdAt: record.createdAt,
@@ -12,6 +12,7 @@ export type ThreadSummary = {
12
12
  createdAt: string;
13
13
  updatedAt: string;
14
14
  status: RunState;
15
+ currentAgentId?: string;
15
16
  };
16
17
  export type SessionSummary = Omit<ThreadSummary, "threadId" | "latestRunId"> & {
17
18
  sessionId: string;
@@ -74,6 +75,28 @@ export type RuntimeToolExecutionToolPolicy = {
74
75
  hasInputSchema: boolean;
75
76
  requiresApproval: boolean;
76
77
  };
78
+ export type RuntimeSnapshotModel = {
79
+ id: string;
80
+ provider: string;
81
+ model: string;
82
+ baseUrl?: string;
83
+ };
84
+ export type RuntimeSnapshotTool = {
85
+ name: string;
86
+ description: string;
87
+ };
88
+ export type RuntimeSnapshotSkill = {
89
+ name: string;
90
+ path: string;
91
+ description?: string;
92
+ };
93
+ export type RuntimeSnapshot = {
94
+ agentId: string;
95
+ model?: RuntimeSnapshotModel;
96
+ tools: RuntimeSnapshotTool[];
97
+ skills: RuntimeSnapshotSkill[];
98
+ memory: string[];
99
+ };
77
100
  /**
78
101
  * Operator-facing projection of tool execution policy already compiled into a binding.
79
102
  * This summarizes existing timeout, retry, validation, and retry-safety hints without
@@ -228,6 +251,12 @@ export type ThreadRunRecord = {
228
251
  state: RunState;
229
252
  checkpointRef: string | null;
230
253
  resumable: boolean;
254
+ startedAt: string;
255
+ endedAt?: string;
256
+ lastActivityAt: string;
257
+ currentAgentId?: string;
258
+ delegationChain: string[];
259
+ runtimeSnapshot?: RuntimeSnapshot;
231
260
  };
232
261
  /**
233
262
  * Persisted run summary projected from upstream execution state plus runtime lifecycle metadata.
@@ -246,6 +275,7 @@ export type RequestRecord = RequestSummary;
246
275
  export type ThreadRecord = {
247
276
  threadId: string;
248
277
  entryAgentId: string;
278
+ currentAgentId?: string;
249
279
  currentState: RunState;
250
280
  latestRunId: string;
251
281
  createdAt: string;
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.137";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.140";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.137";
1
+ export const AGENT_HARNESS_VERSION = "0.0.140";
@@ -1,5 +1,5 @@
1
1
  import type { ArtifactListing, ArtifactRecord, HarnessEvent, InternalApprovalRecord, RunSummary, RunState, ThreadSummary, ThreadRunRecord, TranscriptMessage } from "../contracts/types.js";
2
- import type { ApprovalFilter, PersistedRunRequest, PersistedRunControlRecord, PersistedRunQueueRecord, PersistenceLifecycle as Lifecycle, RuntimePersistence, RecoveryIntent, PersistenceRunMeta as RunMeta, RunSummaryFilter, ThreadSummaryFilter, PersistenceThreadMeta as ThreadMeta } from "./types.js";
2
+ import type { ApprovalFilter, PersistedRunRequest, PersistedRunControlRecord, PersistedRunInspection, PersistedRunQueueRecord, PersistenceLifecycle as Lifecycle, RuntimePersistence, RecoveryIntent, PersistenceRunMeta as RunMeta, RunSummaryFilter, ThreadSummaryFilter, PersistenceThreadMeta as ThreadMeta } from "./types.js";
3
3
  type RunIndexRecord = {
4
4
  runId: string;
5
5
  threadId: string;
@@ -32,6 +32,10 @@ export declare class FilePersistence implements RuntimePersistence {
32
32
  executionMode: string;
33
33
  adapterKind?: string;
34
34
  createdAt: string;
35
+ startedAt?: string;
36
+ currentAgentId?: string;
37
+ delegationChain?: string[];
38
+ runtimeSnapshot?: RunSummary["runtimeSnapshot"];
35
39
  }): Promise<void>;
36
40
  setRunState(threadId: string, runId: string, state: RunState, checkpointRef?: string | null): Promise<void>;
37
41
  appendEvent(event: HarnessEvent): Promise<void>;
@@ -49,6 +53,14 @@ export declare class FilePersistence implements RuntimePersistence {
49
53
  getRunApprovals(threadId: string, runId: string): Promise<InternalApprovalRecord[]>;
50
54
  getRunMeta(threadId: string, runId: string): Promise<RunMeta>;
51
55
  getRunLifecycle(threadId: string, runId: string): Promise<Lifecycle>;
56
+ getRunInspection(threadId: string, runId: string): Promise<PersistedRunInspection>;
57
+ updateRunInspection(threadId: string, runId: string, patch: {
58
+ endedAt?: string | null;
59
+ lastActivityAt?: string;
60
+ currentAgentId?: string | null;
61
+ delegationChain?: string[];
62
+ runtimeSnapshot?: RunSummary["runtimeSnapshot"] | null;
63
+ }): Promise<void>;
52
64
  deleteThread(threadId: string): Promise<boolean>;
53
65
  saveRunRequest(threadId: string, runId: string, request: PersistedRunRequest): Promise<void>;
54
66
  getRunRequest(threadId: string, runId: string): Promise<PersistedRunRequest | null>;
@@ -84,9 +84,18 @@ export class FilePersistence {
84
84
  resumable: false,
85
85
  checkpointRef: null,
86
86
  };
87
+ const inspection = {
88
+ startedAt: input.startedAt ?? input.createdAt,
89
+ endedAt: null,
90
+ lastActivityAt: input.createdAt,
91
+ currentAgentId: input.currentAgentId ?? input.agentId,
92
+ delegationChain: input.delegationChain ?? [input.currentAgentId ?? input.agentId],
93
+ runtimeSnapshot: input.runtimeSnapshot ?? null,
94
+ };
87
95
  await Promise.all([
88
96
  writeJson(path.join(runDir, "meta.json"), meta),
89
97
  writeJson(path.join(runDir, "lifecycle.json"), lifecycle),
98
+ writeJson(path.join(runDir, "inspection.json"), inspection),
90
99
  writeJson(path.join(runDir, "checkpoint-ref.json"), {
91
100
  threadId: input.threadId,
92
101
  runId: input.runId,
@@ -124,9 +133,11 @@ export class FilePersistence {
124
133
  async setRunState(threadId, runId, state, checkpointRef) {
125
134
  const lifecyclePath = path.join(this.runDir(threadId, runId), "lifecycle.json");
126
135
  const runMetaPath = path.join(this.runDir(threadId, runId), "meta.json");
127
- const [lifecycle, runMeta] = await Promise.all([
136
+ const inspectionPath = path.join(this.runDir(threadId, runId), "inspection.json");
137
+ const [lifecycle, runMeta, inspection] = await Promise.all([
128
138
  readJson(lifecyclePath),
129
139
  readJson(runMetaPath),
140
+ readJson(inspectionPath),
130
141
  ]);
131
142
  const now = nowIso();
132
143
  const next = {
@@ -143,6 +154,11 @@ export class FilePersistence {
143
154
  ...runMeta,
144
155
  updatedAt: now,
145
156
  }),
157
+ writeJson(inspectionPath, {
158
+ ...inspection,
159
+ endedAt: state === "completed" || state === "failed" || state === "cancelled" ? now : inspection.endedAt,
160
+ lastActivityAt: now,
161
+ }),
146
162
  ]);
147
163
  if (checkpointRef !== undefined) {
148
164
  await writeJson(path.join(this.runDir(threadId, runId), "checkpoint-ref.json"), {
@@ -175,7 +191,15 @@ export class FilePersistence {
175
191
  }
176
192
  async appendEvent(event) {
177
193
  const sequenceId = String(event.sequence).padStart(6, "0");
178
- await writeJson(path.join(this.runDir(event.threadId, event.runId), "events", `${sequenceId}.json`), event);
194
+ const inspectionPath = path.join(this.runDir(event.threadId, event.runId), "inspection.json");
195
+ const inspection = await readJson(inspectionPath);
196
+ await Promise.all([
197
+ writeJson(path.join(this.runDir(event.threadId, event.runId), "events", `${sequenceId}.json`), event),
198
+ writeJson(inspectionPath, {
199
+ ...inspection,
200
+ lastActivityAt: event.timestamp,
201
+ }),
202
+ ]);
179
203
  }
180
204
  async listSessions(filter = {}) {
181
205
  const threadIndexDir = path.join(this.runRoot, "indexes", "threads");
@@ -185,7 +209,10 @@ export class FilePersistence {
185
209
  const entries = await readdir(threadIndexDir);
186
210
  const records = await Promise.all(entries.map(async (entry) => {
187
211
  const index = await readJson(path.join(threadIndexDir, entry));
188
- const meta = await readJson(path.join(this.threadDir(index.threadId), "meta.json"));
212
+ const [meta, runInspection] = await Promise.all([
213
+ readJson(path.join(this.threadDir(index.threadId), "meta.json")),
214
+ readJson(path.join(this.runDir(index.threadId, index.latestRunId), "inspection.json")).catch(() => null),
215
+ ]);
189
216
  return {
190
217
  agentId: meta.entryAgentId,
191
218
  threadId: index.threadId,
@@ -193,6 +220,7 @@ export class FilePersistence {
193
220
  createdAt: meta.createdAt,
194
221
  updatedAt: index.updatedAt,
195
222
  status: index.status,
223
+ currentAgentId: runInspection?.currentAgentId ?? undefined,
196
224
  };
197
225
  }));
198
226
  return records
@@ -209,9 +237,10 @@ export class FilePersistence {
209
237
  }
210
238
  async readRunSummary(threadId, runId) {
211
239
  const runDir = this.runDir(threadId, runId);
212
- const [meta, lifecycle] = await Promise.all([
240
+ const [meta, lifecycle, inspection] = await Promise.all([
213
241
  readJson(path.join(runDir, "meta.json")),
214
242
  readJson(path.join(runDir, "lifecycle.json")),
243
+ readJson(path.join(runDir, "inspection.json")),
215
244
  ]);
216
245
  return {
217
246
  runId: meta.runId,
@@ -224,6 +253,12 @@ export class FilePersistence {
224
253
  state: lifecycle.state,
225
254
  checkpointRef: lifecycle.checkpointRef,
226
255
  resumable: lifecycle.resumable,
256
+ startedAt: inspection.startedAt,
257
+ ...(inspection.endedAt ? { endedAt: inspection.endedAt } : {}),
258
+ lastActivityAt: inspection.lastActivityAt,
259
+ ...(inspection.currentAgentId ? { currentAgentId: inspection.currentAgentId } : {}),
260
+ delegationChain: inspection.delegationChain,
261
+ ...(inspection.runtimeSnapshot ? { runtimeSnapshot: inspection.runtimeSnapshot } : {}),
227
262
  };
228
263
  }
229
264
  async listRuns(filter = {}) {
@@ -261,6 +296,7 @@ export class FilePersistence {
261
296
  readJson(filePath),
262
297
  readJson(path.join(this.threadDir(threadId), "meta.json")),
263
298
  ]);
299
+ const runInspection = await readJson(path.join(this.runDir(threadId, index.latestRunId), "inspection.json")).catch(() => null);
264
300
  return {
265
301
  agentId: meta.entryAgentId,
266
302
  threadId,
@@ -268,6 +304,7 @@ export class FilePersistence {
268
304
  createdAt: meta.createdAt,
269
305
  updatedAt: index.updatedAt,
270
306
  status: index.status,
307
+ currentAgentId: runInspection?.currentAgentId ?? undefined,
271
308
  };
272
309
  }
273
310
  async getThreadMeta(threadId) {
@@ -335,6 +372,21 @@ export class FilePersistence {
335
372
  async getRunLifecycle(threadId, runId) {
336
373
  return readJson(path.join(this.runDir(threadId, runId), "lifecycle.json"));
337
374
  }
375
+ async getRunInspection(threadId, runId) {
376
+ return readJson(path.join(this.runDir(threadId, runId), "inspection.json"));
377
+ }
378
+ async updateRunInspection(threadId, runId, patch) {
379
+ const inspectionPath = path.join(this.runDir(threadId, runId), "inspection.json");
380
+ const current = await readJson(inspectionPath);
381
+ await writeJson(inspectionPath, {
382
+ ...current,
383
+ ...(patch.endedAt !== undefined ? { endedAt: patch.endedAt } : {}),
384
+ ...(patch.lastActivityAt ? { lastActivityAt: patch.lastActivityAt } : {}),
385
+ ...(patch.currentAgentId !== undefined ? { currentAgentId: patch.currentAgentId } : {}),
386
+ ...(patch.delegationChain ? { delegationChain: patch.delegationChain } : {}),
387
+ ...(patch.runtimeSnapshot !== undefined ? { runtimeSnapshot: patch.runtimeSnapshot } : {}),
388
+ });
389
+ }
338
390
  async deleteThread(threadId) {
339
391
  const threadDir = this.threadDir(threadId);
340
392
  const threadIndexPath = this.threadIndexPath(threadId);
@@ -1,5 +1,5 @@
1
1
  import type { ArtifactListing, ArtifactRecord, HarnessEvent, InternalApprovalRecord, RunState, RunSummary, ThreadRunRecord, ThreadSummary, TranscriptMessage } from "../contracts/types.js";
2
- import type { PersistedRunRequest, PersistedRunControlRecord, PersistedRunQueueRecord, PersistenceLifecycle, PersistenceRunMeta, PersistenceThreadMeta, RecoveryIntent, RuntimePersistence, ApprovalFilter, RunSummaryFilter, ThreadSummaryFilter } from "./types.js";
2
+ import type { PersistedRunRequest, PersistedRunControlRecord, PersistedRunInspection, PersistedRunQueueRecord, PersistenceLifecycle, PersistenceRunMeta, PersistenceThreadMeta, RecoveryIntent, RuntimePersistence, ApprovalFilter, RunSummaryFilter, ThreadSummaryFilter } from "./types.js";
3
3
  export declare function listProtectedCheckpointThreadIds(dbPath: string): Promise<Set<string>>;
4
4
  export declare class SqlitePersistence implements RuntimePersistence {
5
5
  private readonly runRoot;
@@ -43,6 +43,10 @@ export declare class SqlitePersistence implements RuntimePersistence {
43
43
  userMessage: TranscriptMessage;
44
44
  runRequest: PersistedRunRequest;
45
45
  createThread: boolean;
46
+ startedAt?: string;
47
+ currentAgentId?: string;
48
+ delegationChain?: string[];
49
+ runtimeSnapshot?: RunSummary["runtimeSnapshot"];
46
50
  }): Promise<void>;
47
51
  createRun(input: {
48
52
  threadId: string;
@@ -51,6 +55,10 @@ export declare class SqlitePersistence implements RuntimePersistence {
51
55
  executionMode: string;
52
56
  adapterKind?: string;
53
57
  createdAt: string;
58
+ startedAt?: string;
59
+ currentAgentId?: string;
60
+ delegationChain?: string[];
61
+ runtimeSnapshot?: RunSummary["runtimeSnapshot"];
54
62
  }): Promise<void>;
55
63
  setRunState(threadId: string, runId: string, state: RunState, checkpointRef?: string | null): Promise<void>;
56
64
  appendEvent(event: HarnessEvent): Promise<void>;
@@ -66,6 +74,14 @@ export declare class SqlitePersistence implements RuntimePersistence {
66
74
  getRunApprovals(threadId: string, runId: string): Promise<InternalApprovalRecord[]>;
67
75
  getRunMeta(threadId: string, runId: string): Promise<PersistenceRunMeta>;
68
76
  getRunLifecycle(threadId: string, runId: string): Promise<PersistenceLifecycle>;
77
+ getRunInspection(threadId: string, runId: string): Promise<PersistedRunInspection>;
78
+ updateRunInspection(threadId: string, runId: string, patch: {
79
+ endedAt?: string | null;
80
+ lastActivityAt?: string;
81
+ currentAgentId?: string | null;
82
+ delegationChain?: string[];
83
+ runtimeSnapshot?: RunSummary["runtimeSnapshot"] | null;
84
+ }): Promise<void>;
69
85
  deleteThread(threadId: string): Promise<boolean>;
70
86
  saveRunRequest(threadId: string, runId: string, request: PersistedRunRequest): Promise<void>;
71
87
  getRunRequest(threadId: string, runId: string): Promise<PersistedRunRequest | null>;
@@ -4,7 +4,7 @@ import { createClient } from "@libsql/client";
4
4
  import { fileExists, readJson, writeJson } from "../utils/fs.js";
5
5
  import { SqliteRunContextStore } from "./sqlite-run-context-store.js";
6
6
  import { SqliteRunQueueStore } from "./sqlite-run-queue-store.js";
7
- const RUNTIME_SQLITE_SCHEMA_VERSION = 2;
7
+ const RUNTIME_SQLITE_SCHEMA_VERSION = 3;
8
8
  const RUNTIME_SQLITE_SCHEMA_FAMILY = "agent-harness-runtime";
9
9
  function asRow(value) {
10
10
  return value;
@@ -143,10 +143,25 @@ export class SqlitePersistence {
143
143
  checkpoint_ref TEXT,
144
144
  FOREIGN KEY (thread_id) REFERENCES threads(thread_id)
145
145
  )
146
+ `);
147
+ await this.rawExecute(`
148
+ CREATE TABLE IF NOT EXISTS run_inspection (
149
+ run_id TEXT PRIMARY KEY,
150
+ thread_id TEXT NOT NULL,
151
+ started_at TEXT NOT NULL,
152
+ ended_at TEXT,
153
+ last_activity_at TEXT NOT NULL,
154
+ current_agent_id TEXT,
155
+ delegation_chain_json TEXT NOT NULL,
156
+ runtime_snapshot_json TEXT,
157
+ FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
158
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
159
+ )
146
160
  `);
147
161
  await this.rawExecute("CREATE INDEX IF NOT EXISTS runs_thread_updated_idx ON runs(thread_id, updated_at DESC)");
148
162
  await this.rawExecute("CREATE INDEX IF NOT EXISTS runs_state_updated_idx ON runs(state, updated_at DESC)");
149
163
  await this.rawExecute("CREATE INDEX IF NOT EXISTS runs_agent_updated_idx ON runs(agent_id, updated_at DESC)");
164
+ await this.rawExecute("CREATE INDEX IF NOT EXISTS run_inspection_thread_activity_idx ON run_inspection(thread_id, last_activity_at DESC)");
150
165
  await this.rawExecute(`
151
166
  CREATE TABLE IF NOT EXISTS thread_messages (
152
167
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -323,9 +338,11 @@ export class SqlitePersistence {
323
338
  createdAt: asString(row.created_at),
324
339
  updatedAt: asString(row.updated_at),
325
340
  status: asString(row.status),
341
+ currentAgentId: asNullableString(row.current_agent_id) ?? undefined,
326
342
  };
327
343
  }
328
344
  mapRunSummary(row) {
345
+ const runtimeSnapshot = row.runtime_snapshot_json ? parseJson(row.runtime_snapshot_json) : null;
329
346
  return {
330
347
  runId: asString(row.run_id),
331
348
  threadId: asString(row.thread_id),
@@ -337,6 +354,12 @@ export class SqlitePersistence {
337
354
  state: asString(row.state),
338
355
  checkpointRef: asNullableString(row.checkpoint_ref),
339
356
  resumable: asBoolean(row.resumable),
357
+ startedAt: asNullableString(row.started_at) ?? asString(row.created_at),
358
+ endedAt: asNullableString(row.ended_at) ?? undefined,
359
+ lastActivityAt: asNullableString(row.last_activity_at) ?? asString(row.updated_at),
360
+ currentAgentId: asNullableString(row.current_agent_id) ?? undefined,
361
+ delegationChain: row.delegation_chain_json ? parseJson(row.delegation_chain_json) : [asString(row.agent_id)],
362
+ ...(runtimeSnapshot ? { runtimeSnapshot } : {}),
340
363
  };
341
364
  }
342
365
  mapApproval(row) {
@@ -372,7 +395,27 @@ export class SqlitePersistence {
372
395
  if (family !== RUNTIME_SQLITE_SCHEMA_FAMILY) {
373
396
  throw new Error(`Unsupported runtime sqlite schema family ${JSON.stringify(family)} in ${this.dbPath}. Expected ${JSON.stringify(RUNTIME_SQLITE_SCHEMA_FAMILY)}.`);
374
397
  }
375
- if (version === "1") {
398
+ if (version === "1" || version === "2") {
399
+ await this.rawExecute(`
400
+ CREATE TABLE IF NOT EXISTS run_inspection (
401
+ run_id TEXT PRIMARY KEY,
402
+ thread_id TEXT NOT NULL,
403
+ started_at TEXT NOT NULL,
404
+ ended_at TEXT,
405
+ last_activity_at TEXT NOT NULL,
406
+ current_agent_id TEXT,
407
+ delegation_chain_json TEXT NOT NULL,
408
+ runtime_snapshot_json TEXT,
409
+ FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
410
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
411
+ )
412
+ `);
413
+ await this.rawExecute(`
414
+ INSERT OR IGNORE INTO run_inspection
415
+ (run_id, thread_id, started_at, ended_at, last_activity_at, current_agent_id, delegation_chain_json, runtime_snapshot_json)
416
+ SELECT run_id, thread_id, created_at, NULL, updated_at, agent_id, json_array(agent_id), NULL
417
+ FROM runs
418
+ `);
376
419
  await this.rawExecute("INSERT OR REPLACE INTO runtime_metadata (key, value) VALUES (?, ?)", ["schema_version", String(RUNTIME_SQLITE_SCHEMA_VERSION)]);
377
420
  return;
378
421
  }
@@ -422,6 +465,19 @@ export class SqlitePersistence {
422
465
  (run_id, cancel_requested, cancel_reason, cancel_requested_at, heartbeat_at, worker_id, worker_started_at)
423
466
  VALUES (?, 0, NULL, NULL, NULL, NULL, NULL)`,
424
467
  args: [input.runId],
468
+ }, {
469
+ sql: `INSERT OR REPLACE INTO run_inspection
470
+ (run_id, thread_id, started_at, ended_at, last_activity_at, current_agent_id, delegation_chain_json, runtime_snapshot_json)
471
+ VALUES (?, ?, ?, NULL, ?, ?, ?, ?)`,
472
+ args: [
473
+ input.runId,
474
+ input.threadId,
475
+ input.startedAt ?? input.createdAt,
476
+ input.createdAt,
477
+ input.currentAgentId ?? input.agentId,
478
+ JSON.stringify(input.delegationChain ?? [input.currentAgentId ?? input.agentId]),
479
+ input.runtimeSnapshot ? JSON.stringify(input.runtimeSnapshot) : null,
480
+ ],
425
481
  }, {
426
482
  sql: `INSERT INTO thread_messages
427
483
  (thread_id, role, content_json, run_id, created_at)
@@ -463,6 +519,17 @@ export class SqlitePersistence {
463
519
  await this.execute(`INSERT OR REPLACE INTO run_control
464
520
  (run_id, cancel_requested, cancel_reason, cancel_requested_at, heartbeat_at, worker_id, worker_started_at)
465
521
  VALUES (?, 0, NULL, NULL, NULL, NULL, NULL)`, [input.runId]);
522
+ await this.execute(`INSERT OR REPLACE INTO run_inspection
523
+ (run_id, thread_id, started_at, ended_at, last_activity_at, current_agent_id, delegation_chain_json, runtime_snapshot_json)
524
+ VALUES (?, ?, ?, NULL, ?, ?, ?, ?)`, [
525
+ input.runId,
526
+ input.threadId,
527
+ input.startedAt ?? input.createdAt,
528
+ input.createdAt,
529
+ input.currentAgentId ?? input.agentId,
530
+ JSON.stringify(input.delegationChain ?? [input.currentAgentId ?? input.agentId]),
531
+ input.runtimeSnapshot ? JSON.stringify(input.runtimeSnapshot) : null,
532
+ ]);
466
533
  }
467
534
  async setRunState(threadId, runId, state, checkpointRef) {
468
535
  const current = await this.getRunLifecycle(threadId, runId);
@@ -475,42 +542,59 @@ export class SqlitePersistence {
475
542
  await this.execute(`UPDATE threads
476
543
  SET status = ?, latest_run_id = ?, updated_at = ?
477
544
  WHERE thread_id = ?`, [state, runId, now, threadId]);
545
+ await this.execute(`UPDATE run_inspection
546
+ SET last_activity_at = ?, ended_at = CASE
547
+ WHEN ? IN ('completed', 'failed', 'cancelled') THEN ?
548
+ ELSE ended_at
549
+ END
550
+ WHERE run_id = ? AND thread_id = ?`, [now, state, now, runId, threadId]);
478
551
  }
479
552
  async appendEvent(event) {
480
553
  await this.execute(`INSERT OR REPLACE INTO events
481
554
  (thread_id, run_id, sequence, event_json, created_at)
482
555
  VALUES (?, ?, ?, ?, ?)`, [event.threadId, event.runId, event.sequence, JSON.stringify(event), event.timestamp]);
556
+ await this.execute(`UPDATE run_inspection
557
+ SET last_activity_at = ?
558
+ WHERE run_id = ? AND thread_id = ?`, [event.timestamp, event.runId, event.threadId]);
483
559
  }
484
560
  async listSessions(filter = {}) {
485
561
  const { clause, args } = buildWhereClause([
486
- ["entry_agent_id = ?", filter.agentId],
562
+ ["threads.entry_agent_id = ?", filter.agentId],
487
563
  ]);
488
- const rows = await this.selectAll(`SELECT thread_id, entry_agent_id, latest_run_id, created_at, updated_at, status
489
- FROM threads${clause}
564
+ const rows = await this.selectAll(`SELECT threads.thread_id, threads.entry_agent_id, threads.latest_run_id, threads.created_at, threads.updated_at, threads.status, run_inspection.current_agent_id
565
+ FROM threads
566
+ LEFT JOIN run_inspection ON run_inspection.run_id = threads.latest_run_id
567
+ ${clause}
490
568
  ORDER BY updated_at DESC`, args);
491
569
  return rows.map((row) => this.mapThreadSummary(row));
492
570
  }
493
571
  async listRuns(filter = {}) {
494
572
  const { clause, args } = buildWhereClause([
495
- ["agent_id = ?", filter.agentId],
496
- ["thread_id = ?", filter.threadId],
497
- ["state = ?", filter.state],
573
+ ["runs.agent_id = ?", filter.agentId],
574
+ ["runs.thread_id = ?", filter.threadId],
575
+ ["runs.state = ?", filter.state],
498
576
  ]);
499
- const rows = await this.selectAll(`SELECT run_id, thread_id, agent_id, execution_mode, adapter_kind, created_at, updated_at, state, checkpoint_ref, resumable
500
- FROM runs${clause}
577
+ const rows = await this.selectAll(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
578
+ run_inspection.started_at, run_inspection.ended_at, run_inspection.last_activity_at, run_inspection.current_agent_id, run_inspection.delegation_chain_json, run_inspection.runtime_snapshot_json
579
+ FROM runs
580
+ LEFT JOIN run_inspection ON run_inspection.run_id = runs.run_id
581
+ ${clause}
501
582
  ORDER BY updated_at DESC`, args);
502
583
  return rows.map((row) => this.mapRunSummary(row));
503
584
  }
504
585
  async getRun(runId) {
505
- const row = await this.selectOne(`SELECT run_id, thread_id, agent_id, execution_mode, adapter_kind, created_at, updated_at, state, checkpoint_ref, resumable
586
+ const row = await this.selectOne(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
587
+ run_inspection.started_at, run_inspection.ended_at, run_inspection.last_activity_at, run_inspection.current_agent_id, run_inspection.delegation_chain_json, run_inspection.runtime_snapshot_json
506
588
  FROM runs
507
- WHERE run_id = ?`, [runId]);
589
+ LEFT JOIN run_inspection ON run_inspection.run_id = runs.run_id
590
+ WHERE runs.run_id = ?`, [runId]);
508
591
  return row ? this.mapRunSummary(row) : null;
509
592
  }
510
593
  async getSession(threadId) {
511
- const row = await this.selectOne(`SELECT thread_id, entry_agent_id, latest_run_id, created_at, updated_at, status
594
+ const row = await this.selectOne(`SELECT threads.thread_id, threads.entry_agent_id, threads.latest_run_id, threads.created_at, threads.updated_at, threads.status, run_inspection.current_agent_id
512
595
  FROM threads
513
- WHERE thread_id = ?`, [threadId]);
596
+ LEFT JOIN run_inspection ON run_inspection.run_id = threads.latest_run_id
597
+ WHERE threads.thread_id = ?`, [threadId]);
514
598
  return row ? this.mapThreadSummary(row) : null;
515
599
  }
516
600
  async getThreadMeta(threadId) {
@@ -529,9 +613,11 @@ export class SqlitePersistence {
529
613
  };
530
614
  }
531
615
  async listThreadRuns(threadId) {
532
- const rows = await this.selectAll(`SELECT run_id, thread_id, agent_id, execution_mode, adapter_kind, created_at, updated_at, state, checkpoint_ref, resumable
616
+ const rows = await this.selectAll(`SELECT runs.run_id, runs.thread_id, runs.agent_id, runs.execution_mode, runs.adapter_kind, runs.created_at, runs.updated_at, runs.state, runs.checkpoint_ref, runs.resumable,
617
+ run_inspection.started_at, run_inspection.ended_at, run_inspection.last_activity_at, run_inspection.current_agent_id, run_inspection.delegation_chain_json, run_inspection.runtime_snapshot_json
533
618
  FROM runs
534
- WHERE thread_id = ?
619
+ LEFT JOIN run_inspection ON run_inspection.run_id = runs.run_id
620
+ WHERE runs.thread_id = ?
535
621
  ORDER BY created_at DESC`, [threadId]);
536
622
  return rows.map((row) => this.mapRunSummary(row));
537
623
  }
@@ -592,6 +678,36 @@ export class SqlitePersistence {
592
678
  checkpointRef: asNullableString(row.checkpoint_ref),
593
679
  };
594
680
  }
681
+ async getRunInspection(threadId, runId) {
682
+ const row = await this.selectOne(`SELECT started_at, ended_at, last_activity_at, current_agent_id, delegation_chain_json, runtime_snapshot_json
683
+ FROM run_inspection
684
+ WHERE thread_id = ? AND run_id = ?`, [threadId, runId]);
685
+ if (!row) {
686
+ throw new Error(`Missing run inspection ${runId} for thread ${threadId}`);
687
+ }
688
+ return {
689
+ startedAt: asString(row.started_at),
690
+ endedAt: asNullableString(row.ended_at),
691
+ lastActivityAt: asString(row.last_activity_at),
692
+ currentAgentId: asNullableString(row.current_agent_id),
693
+ delegationChain: parseJson(row.delegation_chain_json),
694
+ runtimeSnapshot: row.runtime_snapshot_json ? parseJson(row.runtime_snapshot_json) : null,
695
+ };
696
+ }
697
+ async updateRunInspection(threadId, runId, patch) {
698
+ const current = await this.getRunInspection(threadId, runId);
699
+ await this.execute(`UPDATE run_inspection
700
+ SET ended_at = ?, last_activity_at = ?, current_agent_id = ?, delegation_chain_json = ?, runtime_snapshot_json = ?
701
+ WHERE run_id = ? AND thread_id = ?`, [
702
+ patch.endedAt === undefined ? current.endedAt : patch.endedAt,
703
+ patch.lastActivityAt ?? current.lastActivityAt,
704
+ patch.currentAgentId === undefined ? current.currentAgentId : patch.currentAgentId,
705
+ JSON.stringify(patch.delegationChain ?? current.delegationChain),
706
+ JSON.stringify(patch.runtimeSnapshot === undefined ? current.runtimeSnapshot : patch.runtimeSnapshot),
707
+ runId,
708
+ threadId,
709
+ ]);
710
+ }
595
711
  async deleteThread(threadId) {
596
712
  const exists = await this.getSession(threadId);
597
713
  if (!exists) {
@@ -600,6 +716,7 @@ export class SqlitePersistence {
600
716
  await this.execute("DELETE FROM artifacts WHERE thread_id = ?", [threadId]);
601
717
  await this.execute("DELETE FROM approvals WHERE thread_id = ?", [threadId]);
602
718
  await this.execute("DELETE FROM events WHERE thread_id = ?", [threadId]);
719
+ await this.execute("DELETE FROM run_inspection WHERE thread_id = ?", [threadId]);
603
720
  await this.execute("DELETE FROM run_queue WHERE thread_id = ?", [threadId]);
604
721
  await this.execute("DELETE FROM run_requests WHERE thread_id = ?", [threadId]);
605
722
  await this.execute("DELETE FROM recovery_intents WHERE thread_id = ?", [threadId]);
@@ -1,4 +1,4 @@
1
- import type { ArtifactListing, ArtifactRecord, HarnessEvent, InternalApprovalRecord, InvocationEnvelope, MessageContent, RunState, RunSummary, ThreadRunRecord, ThreadSummary, TranscriptMessage } from "../contracts/types.js";
1
+ import type { ArtifactListing, ArtifactRecord, HarnessEvent, InternalApprovalRecord, InvocationEnvelope, MessageContent, RuntimeSnapshot, RunState, RunSummary, ThreadRunRecord, ThreadSummary, TranscriptMessage } from "../contracts/types.js";
2
2
  export type PersistenceThreadMeta = {
3
3
  threadId: string;
4
4
  workspaceId: string;
@@ -25,6 +25,14 @@ export type PersistenceLifecycle = {
25
25
  resumable: boolean;
26
26
  checkpointRef: string | null;
27
27
  };
28
+ export type PersistedRunInspection = {
29
+ startedAt: string;
30
+ endedAt: string | null;
31
+ lastActivityAt: string;
32
+ currentAgentId: string | null;
33
+ delegationChain: string[];
34
+ runtimeSnapshot: RuntimeSnapshot | null;
35
+ };
28
36
  export type PersistedRunRequest = {
29
37
  input: MessageContent;
30
38
  priority?: number;
@@ -86,6 +94,10 @@ export interface RuntimePersistence {
86
94
  userMessage: TranscriptMessage;
87
95
  runRequest: PersistedRunRequest;
88
96
  createThread: boolean;
97
+ startedAt?: string;
98
+ currentAgentId?: string;
99
+ delegationChain?: string[];
100
+ runtimeSnapshot?: RuntimeSnapshot;
89
101
  }): Promise<void>;
90
102
  createThread(input: {
91
103
  threadId: string;
@@ -101,6 +113,10 @@ export interface RuntimePersistence {
101
113
  executionMode: string;
102
114
  adapterKind?: string;
103
115
  createdAt: string;
116
+ startedAt?: string;
117
+ currentAgentId?: string;
118
+ delegationChain?: string[];
119
+ runtimeSnapshot?: RuntimeSnapshot;
104
120
  }): Promise<void>;
105
121
  setRunState(threadId: string, runId: string, state: RunState, checkpointRef?: string | null): Promise<void>;
106
122
  appendEvent(event: HarnessEvent): Promise<void>;
@@ -115,6 +131,14 @@ export interface RuntimePersistence {
115
131
  getRunApprovals(threadId: string, runId: string): Promise<InternalApprovalRecord[]>;
116
132
  getRunMeta(threadId: string, runId: string): Promise<PersistenceRunMeta>;
117
133
  getRunLifecycle(threadId: string, runId: string): Promise<PersistenceLifecycle>;
134
+ getRunInspection(threadId: string, runId: string): Promise<PersistedRunInspection>;
135
+ updateRunInspection(threadId: string, runId: string, patch: {
136
+ endedAt?: string | null;
137
+ lastActivityAt?: string;
138
+ currentAgentId?: string | null;
139
+ delegationChain?: string[];
140
+ runtimeSnapshot?: RuntimeSnapshot | null;
141
+ }): Promise<void>;
118
142
  deleteThread(threadId: string): Promise<boolean>;
119
143
  saveRunRequest(threadId: string, runId: string, request: PersistedRunRequest): Promise<void>;
120
144
  getRunRequest(threadId: string, runId: string): Promise<PersistedRunRequest | null>;
@@ -1,6 +1,6 @@
1
1
  import type { HarnessEvent, InternalApprovalRecord, MessageContent, RunResult } from "../../../contracts/types.js";
2
2
  import type { RuntimePersistence } from "../../../persistence/types.js";
3
- type EventPersistence = Pick<RuntimePersistence, "appendEvent" | "setRunState" | "createApproval" | "createArtifact">;
3
+ type EventPersistence = Pick<RuntimePersistence, "appendEvent" | "setRunState" | "createApproval" | "createArtifact" | "updateRunInspection">;
4
4
  type EventRuntime = {
5
5
  persistence: EventPersistence;
6
6
  publishEvent: (event: HarnessEvent) => void;
@@ -18,6 +18,11 @@ export async function emitRunCreatedEvent(runtime, threadId, runId, payload) {
18
18
  }
19
19
  export async function setRunStateAndEmitEvent(runtime, threadId, runId, sequence, state, options) {
20
20
  await runtime.persistence.setRunState(threadId, runId, state, options.checkpointRef ?? null);
21
+ const now = new Date(Date.now()).toISOString();
22
+ await runtime.persistence.updateRunInspection(threadId, runId, {
23
+ lastActivityAt: now,
24
+ ...(state === "completed" || state === "failed" || state === "cancelled" ? { endedAt: now } : {}),
25
+ });
21
26
  return emitHarnessEvent(runtime, threadId, runId, sequence, "run.state.changed", {
22
27
  previousState: options.previousState,
23
28
  state,
@@ -6,6 +6,7 @@ export declare function createListenerDispatchRuntime(input: {
6
6
  currentState: RunResult["state"];
7
7
  latestRunId: string;
8
8
  entryAgentId: string;
9
+ currentAgentId?: string;
9
10
  runs: Array<{
10
11
  agentId: string;
11
12
  }>;
@@ -29,6 +29,7 @@ export declare function dispatchRunListeners(stream: AsyncGenerator<InternalHarn
29
29
  currentState: RunResult["state"];
30
30
  latestRunId: string;
31
31
  entryAgentId: string;
32
+ currentAgentId?: string;
32
33
  runs: Array<{
33
34
  agentId: string;
34
35
  }>;
@@ -68,7 +68,7 @@ export async function dispatchRunListeners(stream, listeners, options) {
68
68
  currentState: thread.currentState,
69
69
  latestRunId: thread.latestRunId,
70
70
  entryAgentId: thread.entryAgentId,
71
- latestAgentId: thread.runs[0]?.agentId,
71
+ latestAgentId: thread.currentAgentId ?? thread.runs[0]?.agentId,
72
72
  approvalId: thread.pendingDecision?.approvalId,
73
73
  pendingActionId: thread.pendingDecision?.pendingActionId,
74
74
  output,
@@ -0,0 +1,11 @@
1
+ import type { CompiledAgentBinding, RuntimeSnapshot } from "../../../contracts/types.js";
2
+ export declare function buildRunRuntimeSnapshot(binding: CompiledAgentBinding): RuntimeSnapshot;
3
+ export declare function consumeRunInspectionUpstreamEvent(input: {
4
+ event: unknown;
5
+ currentAgentId: string;
6
+ delegationChain: string[];
7
+ binding: CompiledAgentBinding;
8
+ }): {
9
+ currentAgentId: string;
10
+ delegationChain: string[];
11
+ };
@@ -0,0 +1,97 @@
1
+ import { readSkillMetadata } from "../../support/skill-metadata.js";
2
+ import { getBindingMemorySources, getBindingPrimaryModel, getBindingPrimaryTools, getBindingSkills, getBindingSubagents, } from "../../support/compiled-binding.js";
3
+ function asObject(value) {
4
+ return typeof value === "object" && value !== null ? value : null;
5
+ }
6
+ function readStringArray(value) {
7
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : [];
8
+ }
9
+ export function buildRunRuntimeSnapshot(binding) {
10
+ const model = getBindingPrimaryModel(binding);
11
+ const init = typeof model?.init === "object" && model.init ? model.init : {};
12
+ const baseUrl = typeof init.baseUrl === "string" && init.baseUrl.trim().length > 0
13
+ ? init.baseUrl.trim()
14
+ : typeof init.baseURL === "string" && init.baseURL.trim().length > 0
15
+ ? init.baseURL.trim()
16
+ : undefined;
17
+ return {
18
+ agentId: binding.agent.id,
19
+ ...(model ? {
20
+ model: {
21
+ id: model.id,
22
+ provider: model.provider,
23
+ model: model.model,
24
+ ...(baseUrl ? { baseUrl } : {}),
25
+ },
26
+ } : {}),
27
+ tools: getBindingPrimaryTools(binding).map((tool) => ({
28
+ name: tool.name,
29
+ description: tool.description,
30
+ })),
31
+ skills: getBindingSkills(binding).map((skillPath) => {
32
+ const metadata = readSkillMetadata(skillPath);
33
+ return {
34
+ name: metadata.name,
35
+ path: skillPath,
36
+ ...(metadata.description ? { description: metadata.description } : {}),
37
+ };
38
+ }),
39
+ memory: getBindingMemorySources(binding),
40
+ };
41
+ }
42
+ function maybeAppendAgent(chain, agentId) {
43
+ if (!agentId) {
44
+ return chain;
45
+ }
46
+ if (chain[chain.length - 1] === agentId) {
47
+ return chain;
48
+ }
49
+ if (chain.includes(agentId)) {
50
+ return [...chain.filter((entry) => entry !== agentId), agentId];
51
+ }
52
+ return [...chain, agentId];
53
+ }
54
+ function extractSubagentFromTaskToolEvent(event) {
55
+ const eventName = typeof event.event === "string" ? event.event : "";
56
+ const toolName = typeof event.name === "string" ? event.name : "";
57
+ if (toolName !== "task" || (eventName !== "on_tool_start" && !(eventName === "on_chain_start" && event.run_type === "tool"))) {
58
+ return null;
59
+ }
60
+ const data = asObject(event.data);
61
+ const input = asObject(data?.input);
62
+ const subagentType = typeof input?.subagent_type === "string"
63
+ ? input.subagent_type
64
+ : typeof input?.subagentType === "string"
65
+ ? input.subagentType
66
+ : null;
67
+ return subagentType && subagentType.trim().length > 0 ? subagentType.trim() : null;
68
+ }
69
+ function extractKnownSubagentFromEvent(event, knownSubagentIds) {
70
+ const names = [
71
+ typeof event.name === "string" ? event.name : "",
72
+ ...readStringArray(event.tags),
73
+ ...readStringArray(event.ns),
74
+ ];
75
+ return names.find((name) => knownSubagentIds.has(name)) ?? null;
76
+ }
77
+ export function consumeRunInspectionUpstreamEvent(input) {
78
+ const typed = asObject(input.event);
79
+ if (!typed) {
80
+ return {
81
+ currentAgentId: input.currentAgentId,
82
+ delegationChain: input.delegationChain,
83
+ };
84
+ }
85
+ const knownSubagentIds = new Set(getBindingSubagents(input.binding).map((subagent) => subagent.name));
86
+ const delegatedAgentId = extractSubagentFromTaskToolEvent(typed) ?? extractKnownSubagentFromEvent(typed, knownSubagentIds);
87
+ if (!delegatedAgentId) {
88
+ return {
89
+ currentAgentId: input.currentAgentId,
90
+ delegationChain: input.delegationChain,
91
+ };
92
+ }
93
+ return {
94
+ currentAgentId: delegatedAgentId,
95
+ delegationChain: maybeAppendAgent(input.delegationChain, delegatedAgentId),
96
+ };
97
+ }
@@ -20,6 +20,10 @@ type EnsureThreadStartedRuntime = {
20
20
  };
21
21
  runRequest: PersistedRunRequest;
22
22
  createThread: boolean;
23
+ startedAt?: string;
24
+ currentAgentId?: string;
25
+ delegationChain?: string[];
26
+ runtimeSnapshot?: import("../../../contracts/types.js").RuntimeSnapshot;
23
27
  }) => Promise<void>;
24
28
  createThread: (input: {
25
29
  threadId: string;
@@ -41,6 +45,10 @@ type EnsureThreadStartedRuntime = {
41
45
  executionMode: string;
42
46
  adapterKind: string;
43
47
  createdAt: string;
48
+ startedAt?: string;
49
+ currentAgentId?: string;
50
+ delegationChain?: string[];
51
+ runtimeSnapshot?: import("../../../contracts/types.js").RuntimeSnapshot;
44
52
  }) => Promise<void>;
45
53
  saveRunRequest: (threadId: string, runId: string, runRequest: PersistedRunRequest) => Promise<void>;
46
54
  };
@@ -4,12 +4,17 @@ import { normalizeMessageContent } from "../../../utils/message-content.js";
4
4
  import { buildPersistedRunRequest, normalizeRunPriority } from "./helpers.js";
5
5
  import { getRequiredWorkspaceBinding } from "../bindings.js";
6
6
  import { getBindingAdapterKind, getBindingRuntimeExecutionMode } from "../../support/compiled-binding.js";
7
+ import { buildRunRuntimeSnapshot } from "./inspection.js";
7
8
  export async function ensureThreadStarted(runtime, input) {
8
9
  const { selectedAgentId, binding, message, runRequest, existingThreadId } = input;
9
10
  const threadId = existingThreadId ?? createPersistentId();
10
11
  const runId = createPersistentId();
11
12
  const createdAt = new Date().toISOString();
12
13
  const isNewThread = !existingThreadId;
14
+ const startedAt = createdAt;
15
+ const currentAgentId = selectedAgentId;
16
+ const delegationChain = [selectedAgentId];
17
+ const runtimeSnapshot = buildRunRuntimeSnapshot(binding);
13
18
  const userMessage = {
14
19
  role: "user",
15
20
  content: normalizeMessageContent(message),
@@ -28,6 +33,10 @@ export async function ensureThreadStarted(runtime, input) {
28
33
  userMessage,
29
34
  runRequest,
30
35
  createThread: isNewThread,
36
+ startedAt,
37
+ currentAgentId,
38
+ delegationChain,
39
+ runtimeSnapshot,
31
40
  });
32
41
  }
33
42
  else {
@@ -49,6 +58,10 @@ export async function ensureThreadStarted(runtime, input) {
49
58
  executionMode: getBindingRuntimeExecutionMode(binding),
50
59
  adapterKind: getBindingAdapterKind(binding),
51
60
  createdAt,
61
+ startedAt,
62
+ currentAgentId,
63
+ delegationChain,
64
+ runtimeSnapshot,
52
65
  }),
53
66
  runtime.persistence.saveRunRequest(threadId, runId, runRequest),
54
67
  ]);
@@ -1,4 +1,4 @@
1
- import type { CompiledAgentBinding, HarnessEvent, MessageContent, RunResult, TranscriptMessage } from "../../../contracts/types.js";
1
+ import type { CompiledAgentBinding, HarnessEvent, MessageContent, RunResult, RuntimeSnapshot, TranscriptMessage } from "../../../contracts/types.js";
2
2
  import { type InternalHarnessStreamItem } from "../events/streaming.js";
3
3
  type RuntimeStreamChunk = string | {
4
4
  kind: "content" | "interrupt" | "reasoning" | "step" | "tool-result" | "upstream-event";
@@ -45,6 +45,13 @@ type StreamRunOptions = {
45
45
  }>;
46
46
  appendAssistantMessage: (threadId: string, runId: string, content?: string) => Promise<void>;
47
47
  clearRunRequest: (threadId: string, runId: string) => Promise<void>;
48
+ updateRunInspection: (threadId: string, runId: string, patch: {
49
+ endedAt?: string | null;
50
+ lastActivityAt?: string;
51
+ currentAgentId?: string | null;
52
+ delegationChain?: string[];
53
+ runtimeSnapshot?: RuntimeSnapshot | null;
54
+ }) => Promise<void>;
48
55
  emitSyntheticFallback: (threadId: string, runId: string, selectedAgentId: string, error: unknown) => Promise<void>;
49
56
  };
50
57
  export declare function streamHarnessRun(options: StreamRunOptions): AsyncGenerator<InternalHarnessStreamItem>;
@@ -2,6 +2,7 @@ import { AGENT_INTERRUPT_SENTINEL_PREFIX, RuntimeOperationTimeoutError } from ".
2
2
  import { renderRuntimeFailure, renderToolFailure } from "../../support/harness-support.js";
3
3
  import { getBindingPrimaryModel } from "../../support/compiled-binding.js";
4
4
  import { createContentBlocksItem, createToolResultKey, } from "../events/streaming.js";
5
+ import { consumeRunInspectionUpstreamEvent } from "./inspection.js";
5
6
  function normalizeStreamChunk(chunk) {
6
7
  if (typeof chunk === "string") {
7
8
  if (chunk.startsWith(AGENT_INTERRUPT_SENTINEL_PREFIX)) {
@@ -43,6 +44,8 @@ export async function* streamHarnessRun(options) {
43
44
  let emitted = false;
44
45
  let streamActivityObserved = false;
45
46
  let nonUpstreamStreamActivityObserved = false;
47
+ let currentAgentId = options.selectedAgentId;
48
+ let delegationChain = [options.selectedAgentId];
46
49
  try {
47
50
  const [priorHistory, acquiredReleaseRunSlot] = await Promise.all([
48
51
  priorHistoryPromise,
@@ -64,6 +67,18 @@ export async function* streamHarnessRun(options) {
64
67
  streamActivityObserved = true;
65
68
  const normalizedChunk = normalizeStreamChunk(rawChunk);
66
69
  if (normalizedChunk.kind === "upstream-event") {
70
+ const inspection = consumeRunInspectionUpstreamEvent({
71
+ event: normalizedChunk.event,
72
+ currentAgentId,
73
+ delegationChain,
74
+ binding: options.binding,
75
+ });
76
+ currentAgentId = inspection.currentAgentId;
77
+ delegationChain = inspection.delegationChain;
78
+ await options.updateRunInspection(options.threadId, options.runId, {
79
+ currentAgentId,
80
+ delegationChain,
81
+ });
67
82
  yield {
68
83
  type: "upstream-event",
69
84
  threadId: options.threadId,
@@ -88,7 +103,7 @@ export async function* streamHarnessRun(options) {
88
103
  result: {
89
104
  threadId: options.threadId,
90
105
  runId: options.runId,
91
- agentId: options.selectedAgentId,
106
+ agentId: currentAgentId,
92
107
  state: "waiting_for_approval",
93
108
  output: assistantOutput,
94
109
  finalMessageText: assistantOutput,
@@ -145,7 +160,7 @@ export async function* streamHarnessRun(options) {
145
160
  result: {
146
161
  threadId: options.threadId,
147
162
  runId: options.runId,
148
- agentId: options.selectedAgentId,
163
+ agentId: currentAgentId,
149
164
  state: "completed",
150
165
  output: assistantOutput,
151
166
  finalMessageText: assistantOutput,
@@ -183,7 +198,7 @@ export async function* streamHarnessRun(options) {
183
198
  result: {
184
199
  threadId: options.threadId,
185
200
  runId: options.runId,
186
- agentId: options.selectedAgentId,
201
+ agentId: currentAgentId,
187
202
  state: "failed",
188
203
  output: runtimeFailure,
189
204
  finalMessageText: runtimeFailure,
@@ -232,7 +247,7 @@ export async function* streamHarnessRun(options) {
232
247
  ...actual,
233
248
  threadId: options.threadId,
234
249
  runId: options.runId,
235
- agentId: options.selectedAgentId,
250
+ agentId: currentAgentId,
236
251
  },
237
252
  };
238
253
  yield {
@@ -265,7 +280,7 @@ export async function* streamHarnessRun(options) {
265
280
  result: {
266
281
  threadId: options.threadId,
267
282
  runId: options.runId,
268
- agentId: options.selectedAgentId,
283
+ agentId: currentAgentId,
269
284
  state: "failed",
270
285
  output: runtimeFailure,
271
286
  finalMessageText: runtimeFailure,
@@ -20,6 +20,7 @@ export async function buildThreadInspectionRecord(input, threadId) {
20
20
  return {
21
21
  threadId,
22
22
  entryAgentId: meta.entryAgentId,
23
+ currentAgentId: threadSummary.currentAgentId,
23
24
  currentState: threadSummary.status,
24
25
  latestRunId,
25
26
  createdAt: meta.createdAt,
@@ -0,0 +1,24 @@
1
+ import type { HarnessEvent, HarnessEventProjection } from "../../../contracts/types.js";
2
+ import type { RuntimePersistence } from "../../../persistence/types.js";
3
+ import type { StoreLike } from "./store.js";
4
+ export type ResolvedRuntimeMemorySyncConfig = {
5
+ enabled: true;
6
+ writeOnApprovalResolution: boolean;
7
+ writeOnRunCompletion: boolean;
8
+ backgroundConsolidation: boolean;
9
+ maxMessagesPerRun: number;
10
+ };
11
+ export declare function readRuntimeMemorySyncConfig(runtimeMemory: Record<string, unknown> | undefined): ResolvedRuntimeMemorySyncConfig | undefined;
12
+ export declare class RuntimeMemorySync implements HarnessEventProjection {
13
+ private readonly persistence;
14
+ readonly store: StoreLike;
15
+ private readonly config;
16
+ private readonly pending;
17
+ private syncChain;
18
+ readonly name = "runtime-memory-sync";
19
+ constructor(persistence: RuntimePersistence, store: StoreLike, config: ResolvedRuntimeMemorySyncConfig);
20
+ shouldHandle(event: HarnessEvent): boolean;
21
+ handleEvent(event: HarnessEvent): Promise<void>;
22
+ private syncRun;
23
+ close(): Promise<void>;
24
+ }
@@ -0,0 +1,193 @@
1
+ import { extractMessageText } from "../../../utils/message-content.js";
2
+ function asRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
4
+ }
5
+ function asBoolean(value) {
6
+ return typeof value === "boolean" ? value : undefined;
7
+ }
8
+ function asPositiveInteger(value) {
9
+ return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
10
+ }
11
+ function excerpt(message) {
12
+ if (!message?.content) {
13
+ return "(none)";
14
+ }
15
+ const normalized = extractMessageText(message.content).replace(/\s+/g, " ").trim();
16
+ return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
17
+ }
18
+ function formatMessageSection(title, messages) {
19
+ const lines = [`## ${title}`, ""];
20
+ if (messages.length === 0) {
21
+ lines.push("(none)", "");
22
+ return lines;
23
+ }
24
+ for (const message of messages) {
25
+ lines.push(`- ${excerpt(message)}`);
26
+ }
27
+ lines.push("");
28
+ return lines;
29
+ }
30
+ function formatApprovalSection(approvals) {
31
+ const lines = ["## Approval Snapshot", ""];
32
+ if (approvals.length === 0) {
33
+ lines.push("(none)", "");
34
+ return lines;
35
+ }
36
+ for (const approval of approvals) {
37
+ lines.push(`- ${approval.approvalId}: ${approval.toolName} (${approval.status})`);
38
+ }
39
+ lines.push("");
40
+ return lines;
41
+ }
42
+ function renderRunSummaryMarkdown(input) {
43
+ const userMessages = input.messages.filter((message) => message.role === "user").slice(-3);
44
+ const assistantMessages = input.messages.filter((message) => message.role === "assistant").slice(-3);
45
+ return [
46
+ "# Run Memory Summary",
47
+ "",
48
+ `- thread_id: ${input.thread.threadId}`,
49
+ `- run_id: ${input.runId}`,
50
+ `- agent_id: ${input.agentId}`,
51
+ `- status: ${input.thread.status}`,
52
+ `- trigger: ${input.trigger}`,
53
+ `- captured_at: ${input.capturedAt}`,
54
+ "",
55
+ ...formatMessageSection("Recent User Messages", userMessages),
56
+ ...formatMessageSection("Recent Assistant Messages", assistantMessages),
57
+ ...formatApprovalSection(input.approvals),
58
+ ].join("\n");
59
+ }
60
+ function renderThreadDigestMarkdown(input) {
61
+ const latestUser = input.messages.filter((message) => message.role === "user").at(-1);
62
+ const latestAssistant = input.messages.filter((message) => message.role === "assistant").at(-1);
63
+ return [
64
+ "# Durable Thread Digest",
65
+ "",
66
+ `- thread_id: ${input.thread.threadId}`,
67
+ `- latest_run_id: ${input.runId}`,
68
+ `- status: ${input.thread.status}`,
69
+ `- last_memory_trigger: ${input.trigger}`,
70
+ `- updated_at: ${input.capturedAt}`,
71
+ "",
72
+ "## Latest Durable User Context",
73
+ excerpt(latestUser),
74
+ "",
75
+ "## Latest Durable Assistant Context",
76
+ excerpt(latestAssistant),
77
+ "",
78
+ ].join("\n");
79
+ }
80
+ const RUNTIME_MEMORY_EVENT_TYPES = new Set(["run.state.changed", "approval.resolved"]);
81
+ export function readRuntimeMemorySyncConfig(runtimeMemory) {
82
+ if (runtimeMemory?.enabled !== true) {
83
+ return undefined;
84
+ }
85
+ const ingestion = asRecord(runtimeMemory.ingestion);
86
+ const writeOnApprovalResolution = asBoolean(ingestion?.writeOnApprovalResolution) ?? true;
87
+ const writeOnRunCompletion = asBoolean(ingestion?.writeOnRunCompletion) ?? true;
88
+ if (!writeOnApprovalResolution && !writeOnRunCompletion) {
89
+ return undefined;
90
+ }
91
+ return {
92
+ enabled: true,
93
+ writeOnApprovalResolution,
94
+ writeOnRunCompletion,
95
+ backgroundConsolidation: asBoolean(ingestion?.backgroundConsolidation) ?? true,
96
+ maxMessagesPerRun: asPositiveInteger(ingestion?.maxMessagesPerRun) ?? 40,
97
+ };
98
+ }
99
+ export class RuntimeMemorySync {
100
+ persistence;
101
+ store;
102
+ config;
103
+ pending = new Set();
104
+ syncChain = Promise.resolve();
105
+ name = "runtime-memory-sync";
106
+ constructor(persistence, store, config) {
107
+ this.persistence = persistence;
108
+ this.store = store;
109
+ this.config = config;
110
+ }
111
+ shouldHandle(event) {
112
+ if (!RUNTIME_MEMORY_EVENT_TYPES.has(event.eventType)) {
113
+ return false;
114
+ }
115
+ if (event.eventType === "approval.resolved") {
116
+ return this.config.writeOnApprovalResolution;
117
+ }
118
+ return this.config.writeOnRunCompletion && event.payload.state === "completed";
119
+ }
120
+ async handleEvent(event) {
121
+ if (!this.shouldHandle(event)) {
122
+ return;
123
+ }
124
+ const trigger = event.eventType === "approval.resolved" ? "approval.resolved" : "run.completed";
125
+ const task = this.syncChain
126
+ .then(() => this.syncRun(event.threadId, event.runId, trigger, event.timestamp))
127
+ .catch(() => {
128
+ // Fail open: runtime memory sync must not break the hot path.
129
+ });
130
+ this.syncChain = task
131
+ .catch(() => {
132
+ // Fail open: runtime memory sync must not break the hot path.
133
+ })
134
+ .finally(() => {
135
+ this.pending.delete(task);
136
+ });
137
+ this.pending.add(task);
138
+ }
139
+ async syncRun(threadId, runId, trigger, capturedAt) {
140
+ const [thread, run, allMessages, approvals] = await Promise.all([
141
+ this.persistence.getSession(threadId),
142
+ this.persistence.getRun(runId),
143
+ this.persistence.listThreadMessages(threadId, this.config.maxMessagesPerRun),
144
+ this.persistence.getRunApprovals(threadId, runId),
145
+ ]);
146
+ if (!thread || !run) {
147
+ return;
148
+ }
149
+ const messages = allMessages.filter((message) => message.runId === runId);
150
+ if (messages.length === 0) {
151
+ return;
152
+ }
153
+ const agentId = run.agentId ?? thread.agentId;
154
+ const summaryMarkdown = renderRunSummaryMarkdown({
155
+ thread,
156
+ runId,
157
+ agentId,
158
+ trigger,
159
+ capturedAt,
160
+ messages,
161
+ approvals,
162
+ });
163
+ await Promise.all([
164
+ this.store.put(["memories", "runs", threadId], `${runId}.summary.md`, { content: `${summaryMarkdown}\n` }),
165
+ this.store.put(["memories", "runs", threadId], `${runId}.record.json`, {
166
+ kind: "summary",
167
+ scope: "thread",
168
+ threadId,
169
+ runId,
170
+ agentId,
171
+ trigger,
172
+ capturedAt,
173
+ status: thread.status,
174
+ messageCount: messages.length,
175
+ approvalCount: approvals.length,
176
+ }),
177
+ ]);
178
+ if (!this.config.backgroundConsolidation) {
179
+ return;
180
+ }
181
+ const digestMarkdown = renderThreadDigestMarkdown({
182
+ thread,
183
+ runId,
184
+ trigger,
185
+ capturedAt,
186
+ messages,
187
+ });
188
+ await this.store.put(["memories", "threads", threadId], "durable-summary.md", { content: `${digestMarkdown}\n` });
189
+ }
190
+ async close() {
191
+ await Promise.allSettled(Array.from(this.pending));
192
+ }
193
+ }
@@ -23,6 +23,8 @@ export declare class AgentHarnessRuntime {
23
23
  private readonly routingDefaultAgentId?;
24
24
  private readonly threadMemorySync;
25
25
  private readonly unregisterThreadMemorySync;
26
+ private readonly runtimeMemorySync;
27
+ private readonly unregisterRuntimeMemorySync;
26
28
  private readonly mem0IngestionSync;
27
29
  private readonly unregisterMem0IngestionSync;
28
30
  private readonly resolvedRuntimeAdapterOptions;
@@ -7,6 +7,7 @@ import { PolicyEngine } from "./harness/system/policy-engine.js";
7
7
  import { getConcurrencyConfig, getRecoveryConfig, getRoutingDefaultAgentId, getRoutingRules, } from "../workspace/support/workspace-ref-utils.js";
8
8
  import { createHarnessEvent, inferRoutingBindings, renderRuntimeFailure, } from "./support/harness-support.js";
9
9
  import { ThreadMemorySync } from "./harness/system/thread-memory-sync.js";
10
+ import { RuntimeMemorySync, readRuntimeMemorySyncConfig } from "./harness/system/runtime-memory-sync.js";
10
11
  import { FileBackedStore } from "./harness/system/store.js";
11
12
  import { HealthMonitor, readHealthMonitorConfig, } from "./harness/system/health-monitor.js";
12
13
  import { normalizeInvocationEnvelope, normalizeRunPriority, resolveRunListeners, } from "./harness/run/helpers.js";
@@ -58,6 +59,8 @@ export class AgentHarnessRuntime {
58
59
  routingDefaultAgentId;
59
60
  threadMemorySync;
60
61
  unregisterThreadMemorySync;
62
+ runtimeMemorySync;
63
+ unregisterRuntimeMemorySync;
61
64
  mem0IngestionSync;
62
65
  unregisterMem0IngestionSync;
63
66
  resolvedRuntimeAdapterOptions;
@@ -143,6 +146,15 @@ export class AgentHarnessRuntime {
143
146
  this.threadMemorySync = null;
144
147
  this.unregisterThreadMemorySync = () => { };
145
148
  }
149
+ const runtimeMemorySyncConfig = readRuntimeMemorySyncConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory);
150
+ if (runtimeMemorySyncConfig) {
151
+ this.runtimeMemorySync = new RuntimeMemorySync(this.persistence, this.runtimeMemoryStore, runtimeMemorySyncConfig);
152
+ this.unregisterRuntimeMemorySync = this.eventBus.registerProjection(this.runtimeMemorySync);
153
+ }
154
+ else {
155
+ this.runtimeMemorySync = null;
156
+ this.unregisterRuntimeMemorySync = () => { };
157
+ }
146
158
  const mem0RuntimeConfig = readMem0RuntimeConfig(this.defaultRuntimeEntryBinding?.harnessRuntime.runtimeMemory, this.workspace.workspaceRoot);
147
159
  if (mem0RuntimeConfig) {
148
160
  this.mem0IngestionSync = new Mem0IngestionSync(this.persistence, mem0RuntimeConfig, runRoot);
@@ -517,6 +529,7 @@ export class AgentHarnessRuntime {
517
529
  requestApprovalAndEmit: (threadId, runId, input, interruptContent, checkpointRef, sequence) => this.requestApprovalAndEmit(threadId, runId, input, interruptContent, checkpointRef, sequence),
518
530
  appendAssistantMessage: (threadId, runId, content) => appendLifecycleAssistantMessage(this.persistence, threadId, runId, content),
519
531
  clearRunRequest: (threadId, runId) => this.persistence.clearRunRequest(threadId, runId),
532
+ updateRunInspection: (threadId, runId, patch) => this.persistence.updateRunInspection(threadId, runId, patch),
520
533
  emitSyntheticFallback: (threadId, runId, selectedAgentId, error) => this.runtimeEventOperations.emitSyntheticFallback(threadId, runId, selectedAgentId, error),
521
534
  });
522
535
  for await (const item of stream) {
@@ -581,9 +594,11 @@ export class AgentHarnessRuntime {
581
594
  this.closed = true;
582
595
  await this.healthMonitor?.stop();
583
596
  this.unregisterThreadMemorySync();
597
+ this.unregisterRuntimeMemorySync();
584
598
  this.unregisterMem0IngestionSync();
585
599
  await Promise.allSettled(Array.from(this.backgroundTasks));
586
600
  await this.threadMemorySync?.close();
601
+ await this.runtimeMemorySync?.close();
587
602
  await this.mem0IngestionSync?.close();
588
603
  await closeMcpClientsForWorkspace(this.workspace);
589
604
  this.initialized = false;
@@ -91,6 +91,7 @@ export async function maintainSqliteRuntimeRecords(dbPath, config, nowMs = Date.
91
91
  "DELETE FROM artifacts WHERE thread_id = ?",
92
92
  "DELETE FROM approvals WHERE thread_id = ?",
93
93
  "DELETE FROM events WHERE thread_id = ?",
94
+ "DELETE FROM run_inspection WHERE thread_id = ?",
94
95
  "DELETE FROM run_queue WHERE thread_id = ?",
95
96
  "DELETE FROM run_requests WHERE thread_id = ?",
96
97
  "DELETE FROM recovery_intents WHERE thread_id = ?",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.138",
3
+ "version": "0.0.141",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",