@botbotgo/agent-harness 0.0.80 → 0.0.82

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.
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { mkdir, rm } from "node:fs/promises";
3
3
  import { createClient } from "@libsql/client";
4
4
  import { fileExists, readJson, writeJson } from "../utils/fs.js";
5
- const RUNTIME_SQLITE_SCHEMA_VERSION = 1;
5
+ const RUNTIME_SQLITE_SCHEMA_VERSION = 2;
6
6
  const RUNTIME_SQLITE_SCHEMA_FAMILY = "agent-harness-runtime";
7
7
  function asRow(value) {
8
8
  return value;
@@ -25,6 +25,16 @@ function toSqliteUrl(filePath) {
25
25
  function nowIso() {
26
26
  return new Date(Date.now()).toISOString();
27
27
  }
28
+ function buildWhereClause(filters) {
29
+ const active = filters.filter(([, value]) => value !== undefined);
30
+ if (active.length === 0) {
31
+ return { clause: "", args: [] };
32
+ }
33
+ return {
34
+ clause: ` WHERE ${active.map(([sql]) => sql).join(" AND ")}`,
35
+ args: active.map(([, value]) => value),
36
+ };
37
+ }
28
38
  async function selectProtectedThreadIds(dbPath) {
29
39
  if (!(await fileExists(dbPath))) {
30
40
  return [];
@@ -187,6 +197,37 @@ export class SqlitePersistence {
187
197
  FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
188
198
  FOREIGN KEY (run_id) REFERENCES runs(run_id)
189
199
  )
200
+ `);
201
+ await this.rawExecute(`
202
+ CREATE TABLE IF NOT EXISTS run_queue (
203
+ run_id TEXT PRIMARY KEY,
204
+ thread_id TEXT NOT NULL,
205
+ priority INTEGER NOT NULL DEFAULT 0,
206
+ queue_key TEXT,
207
+ enqueued_at TEXT NOT NULL,
208
+ available_at TEXT NOT NULL,
209
+ claimed_by TEXT,
210
+ claimed_at TEXT,
211
+ lease_expires_at TEXT,
212
+ attempt_count INTEGER NOT NULL DEFAULT 0,
213
+ last_error TEXT,
214
+ FOREIGN KEY (thread_id) REFERENCES threads(thread_id),
215
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
216
+ )
217
+ `);
218
+ await this.rawExecute("CREATE INDEX IF NOT EXISTS run_queue_available_idx ON run_queue(available_at, priority DESC, enqueued_at ASC)");
219
+ await this.rawExecute("CREATE INDEX IF NOT EXISTS run_queue_claim_idx ON run_queue(claimed_by, lease_expires_at)");
220
+ await this.rawExecute(`
221
+ CREATE TABLE IF NOT EXISTS run_control (
222
+ run_id TEXT PRIMARY KEY,
223
+ cancel_requested INTEGER NOT NULL DEFAULT 0,
224
+ cancel_reason TEXT,
225
+ cancel_requested_at TEXT,
226
+ heartbeat_at TEXT,
227
+ worker_id TEXT,
228
+ worker_started_at TEXT,
229
+ FOREIGN KEY (run_id) REFERENCES runs(run_id)
230
+ )
190
231
  `);
191
232
  await this.rawExecute(`
192
233
  CREATE TABLE IF NOT EXISTS artifacts (
@@ -292,6 +333,10 @@ export class SqlitePersistence {
292
333
  if (family !== RUNTIME_SQLITE_SCHEMA_FAMILY) {
293
334
  throw new Error(`Unsupported runtime sqlite schema family ${JSON.stringify(family)} in ${this.dbPath}. Expected ${JSON.stringify(RUNTIME_SQLITE_SCHEMA_FAMILY)}.`);
294
335
  }
336
+ if (version === "1") {
337
+ await this.rawExecute("INSERT OR REPLACE INTO runtime_metadata (key, value) VALUES (?, ?)", ["schema_version", String(RUNTIME_SQLITE_SCHEMA_VERSION)]);
338
+ return;
339
+ }
295
340
  if (version !== String(RUNTIME_SQLITE_SCHEMA_VERSION)) {
296
341
  throw new Error(`Unsupported runtime sqlite schema version ${JSON.stringify(version)} in ${this.dbPath}. Expected ${RUNTIME_SQLITE_SCHEMA_VERSION}.`);
297
342
  }
@@ -321,6 +366,9 @@ export class SqlitePersistence {
321
366
  0,
322
367
  null,
323
368
  ]);
369
+ await this.execute(`INSERT OR REPLACE INTO run_control
370
+ (run_id, cancel_requested, cancel_reason, cancel_requested_at, heartbeat_at, worker_id, worker_started_at)
371
+ VALUES (?, 0, NULL, NULL, NULL, NULL, NULL)`, [input.runId]);
324
372
  }
325
373
  async setRunState(threadId, runId, state, checkpointRef) {
326
374
  const current = await this.getRunLifecycle(threadId, runId);
@@ -339,16 +387,24 @@ export class SqlitePersistence {
339
387
  (thread_id, run_id, sequence, event_json, created_at)
340
388
  VALUES (?, ?, ?, ?, ?)`, [event.threadId, event.runId, event.sequence, JSON.stringify(event), event.timestamp]);
341
389
  }
342
- async listSessions() {
390
+ async listSessions(filter = {}) {
391
+ const { clause, args } = buildWhereClause([
392
+ ["entry_agent_id = ?", filter.agentId],
393
+ ]);
343
394
  const rows = await this.selectAll(`SELECT thread_id, entry_agent_id, latest_run_id, created_at, updated_at, status
344
- FROM threads
345
- ORDER BY updated_at DESC`);
395
+ FROM threads${clause}
396
+ ORDER BY updated_at DESC`, args);
346
397
  return rows.map((row) => this.mapThreadSummary(row));
347
398
  }
348
- async listRuns() {
399
+ async listRuns(filter = {}) {
400
+ const { clause, args } = buildWhereClause([
401
+ ["agent_id = ?", filter.agentId],
402
+ ["thread_id = ?", filter.threadId],
403
+ ["state = ?", filter.state],
404
+ ]);
349
405
  const rows = await this.selectAll(`SELECT run_id, thread_id, agent_id, execution_mode, adapter_kind, created_at, updated_at, state, checkpoint_ref, resumable
350
- FROM runs
351
- ORDER BY updated_at DESC`);
406
+ FROM runs${clause}
407
+ ORDER BY updated_at DESC`, args);
352
408
  return rows.map((row) => this.mapRunSummary(row));
353
409
  }
354
410
  async getRun(runId) {
@@ -392,8 +448,13 @@ export class SqlitePersistence {
392
448
  ORDER BY sequence ASC`, [threadId, runId]);
393
449
  return rows.map((row) => parseJson(row.event_json));
394
450
  }
395
- async listApprovals() {
396
- const rows = await this.selectAll("SELECT * FROM approvals ORDER BY requested_at ASC, approval_id ASC");
451
+ async listApprovals(filter = {}) {
452
+ const { clause, args } = buildWhereClause([
453
+ ["status = ?", filter.status],
454
+ ["thread_id = ?", filter.threadId],
455
+ ["run_id = ?", filter.runId],
456
+ ]);
457
+ const rows = await this.selectAll(`SELECT * FROM approvals${clause} ORDER BY requested_at ASC, approval_id ASC`, args);
397
458
  return rows.map((row) => this.mapApproval(row));
398
459
  }
399
460
  async getApproval(approvalId) {
@@ -445,9 +506,11 @@ export class SqlitePersistence {
445
506
  await this.execute("DELETE FROM artifacts WHERE thread_id = ?", [threadId]);
446
507
  await this.execute("DELETE FROM approvals WHERE thread_id = ?", [threadId]);
447
508
  await this.execute("DELETE FROM events WHERE thread_id = ?", [threadId]);
509
+ await this.execute("DELETE FROM run_queue WHERE thread_id = ?", [threadId]);
448
510
  await this.execute("DELETE FROM run_requests WHERE thread_id = ?", [threadId]);
449
511
  await this.execute("DELETE FROM recovery_intents WHERE thread_id = ?", [threadId]);
450
512
  await this.execute("DELETE FROM thread_messages WHERE thread_id = ?", [threadId]);
513
+ await this.execute("DELETE FROM run_control WHERE run_id IN (SELECT run_id FROM runs WHERE thread_id = ?)", [threadId]);
451
514
  await this.execute("DELETE FROM runs WHERE thread_id = ?", [threadId]);
452
515
  await this.execute("DELETE FROM threads WHERE thread_id = ?", [threadId]);
453
516
  await rm(this.threadDir(threadId), { recursive: true, force: true });
@@ -559,6 +622,108 @@ export class SqlitePersistence {
559
622
  async clearRecoveryIntent(threadId, runId) {
560
623
  await this.execute("DELETE FROM recovery_intents WHERE thread_id = ? AND run_id = ?", [threadId, runId]);
561
624
  }
625
+ mapQueuedRun(row) {
626
+ return {
627
+ runId: asString(row.run_id),
628
+ threadId: asString(row.thread_id),
629
+ priority: Number(row.priority ?? 0),
630
+ queueKey: asNullableString(row.queue_key),
631
+ enqueuedAt: asString(row.enqueued_at),
632
+ availableAt: asString(row.available_at),
633
+ claimedBy: asNullableString(row.claimed_by),
634
+ claimedAt: asNullableString(row.claimed_at),
635
+ leaseExpiresAt: asNullableString(row.lease_expires_at),
636
+ attemptCount: Number(row.attempt_count ?? 0),
637
+ lastError: asNullableString(row.last_error),
638
+ };
639
+ }
640
+ mapRunControl(row) {
641
+ return {
642
+ runId: asString(row.run_id),
643
+ cancelRequested: asBoolean(row.cancel_requested),
644
+ cancelReason: asNullableString(row.cancel_reason),
645
+ cancelRequestedAt: asNullableString(row.cancel_requested_at),
646
+ heartbeatAt: asNullableString(row.heartbeat_at),
647
+ workerId: asNullableString(row.worker_id),
648
+ workerStartedAt: asNullableString(row.worker_started_at),
649
+ };
650
+ }
651
+ async enqueueRun(input) {
652
+ const enqueuedAt = nowIso();
653
+ await this.execute(`INSERT OR REPLACE INTO run_queue
654
+ (run_id, thread_id, priority, queue_key, enqueued_at, available_at, claimed_by, claimed_at, lease_expires_at, attempt_count, last_error)
655
+ VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, COALESCE((SELECT attempt_count FROM run_queue WHERE run_id = ?), 0), NULL)`, [
656
+ input.runId,
657
+ input.threadId,
658
+ input.priority ?? 0,
659
+ input.queueKey ?? null,
660
+ enqueuedAt,
661
+ input.availableAt ?? enqueuedAt,
662
+ input.runId,
663
+ ]);
664
+ }
665
+ async getQueuedRun(runId) {
666
+ const row = await this.selectOne("SELECT * FROM run_queue WHERE run_id = ?", [runId]);
667
+ return row ? this.mapQueuedRun(row) : null;
668
+ }
669
+ async claimQueuedRun(input) {
670
+ const claimedAt = input.claimedAt ?? nowIso();
671
+ await this.execute(`UPDATE run_queue
672
+ SET claimed_by = ?, claimed_at = ?, lease_expires_at = ?, attempt_count = attempt_count + 1
673
+ WHERE run_id = ? AND thread_id = ?`, [input.workerId, claimedAt, input.leaseExpiresAt, input.runId, input.threadId]);
674
+ await this.execute(`UPDATE run_control
675
+ SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
676
+ WHERE run_id = ?`, [claimedAt, input.workerId, claimedAt, input.runId]);
677
+ const claimed = await this.getQueuedRun(input.runId);
678
+ if (!claimed) {
679
+ throw new Error(`Missing queued run ${input.runId}`);
680
+ }
681
+ return claimed;
682
+ }
683
+ async renewRunLease(input) {
684
+ const heartbeatAt = input.heartbeatAt ?? nowIso();
685
+ await this.execute(`UPDATE run_queue
686
+ SET lease_expires_at = ?, claimed_by = COALESCE(claimed_by, ?), claimed_at = COALESCE(claimed_at, ?)
687
+ WHERE run_id = ?`, [input.leaseExpiresAt, input.workerId, heartbeatAt, input.runId]);
688
+ await this.execute(`UPDATE run_control
689
+ SET heartbeat_at = ?, worker_id = ?, worker_started_at = COALESCE(worker_started_at, ?)
690
+ WHERE run_id = ?`, [heartbeatAt, input.workerId, heartbeatAt, input.runId]);
691
+ }
692
+ async releaseRunClaim(runId) {
693
+ await this.execute("DELETE FROM run_queue WHERE run_id = ?", [runId]);
694
+ await this.execute(`UPDATE run_control
695
+ SET heartbeat_at = NULL, worker_id = NULL, worker_started_at = NULL
696
+ WHERE run_id = ?`, [runId]);
697
+ }
698
+ async listExpiredClaimedRuns(cutoffIso) {
699
+ const rows = await this.selectAll(`SELECT *
700
+ FROM run_queue
701
+ WHERE claimed_by IS NOT NULL
702
+ AND lease_expires_at IS NOT NULL
703
+ AND lease_expires_at <= ?
704
+ ORDER BY lease_expires_at ASC, run_id ASC`, [cutoffIso]);
705
+ return rows.map((row) => this.mapQueuedRun(row));
706
+ }
707
+ async getRunControl(runId) {
708
+ const row = await this.selectOne("SELECT * FROM run_control WHERE run_id = ?", [runId]);
709
+ return row ? this.mapRunControl(row) : null;
710
+ }
711
+ async requestRunCancel(runId, reason) {
712
+ const now = nowIso();
713
+ await this.execute(`UPDATE run_control
714
+ SET cancel_requested = 1, cancel_reason = ?, cancel_requested_at = ?
715
+ WHERE run_id = ?`, [reason ?? null, now, runId]);
716
+ const updated = await this.getRunControl(runId);
717
+ if (!updated) {
718
+ throw new Error(`Missing run control for ${runId}`);
719
+ }
720
+ return updated;
721
+ }
722
+ async clearRunCancel(runId) {
723
+ await this.execute(`UPDATE run_control
724
+ SET cancel_requested = 0, cancel_reason = NULL, cancel_requested_at = NULL
725
+ WHERE run_id = ?`, [runId]);
726
+ }
562
727
  async readArtifact(threadId, runId, artifactPath) {
563
728
  const filePath = path.join(this.runDir(threadId, runId), artifactPath);
564
729
  if (!(await fileExists(filePath))) {
@@ -37,6 +37,41 @@ export type RecoveryIntent = {
37
37
  resumePayload: unknown;
38
38
  attempts: number;
39
39
  };
40
+ export type PersistedRunQueueRecord = {
41
+ runId: string;
42
+ threadId: string;
43
+ priority: number;
44
+ queueKey: string | null;
45
+ enqueuedAt: string;
46
+ availableAt: string;
47
+ claimedBy: string | null;
48
+ claimedAt: string | null;
49
+ leaseExpiresAt: string | null;
50
+ attemptCount: number;
51
+ lastError: string | null;
52
+ };
53
+ export type PersistedRunControlRecord = {
54
+ runId: string;
55
+ cancelRequested: boolean;
56
+ cancelReason: string | null;
57
+ cancelRequestedAt: string | null;
58
+ heartbeatAt: string | null;
59
+ workerId: string | null;
60
+ workerStartedAt: string | null;
61
+ };
62
+ export type ThreadSummaryFilter = {
63
+ agentId?: string;
64
+ };
65
+ export type RunSummaryFilter = {
66
+ agentId?: string;
67
+ threadId?: string;
68
+ state?: RunSummary["state"];
69
+ };
70
+ export type ApprovalFilter = {
71
+ status?: InternalApprovalRecord["status"];
72
+ threadId?: string;
73
+ runId?: string;
74
+ };
40
75
  export interface RuntimePersistence {
41
76
  initialize(): Promise<void>;
42
77
  createThread(input: {
@@ -56,13 +91,13 @@ export interface RuntimePersistence {
56
91
  }): Promise<void>;
57
92
  setRunState(threadId: string, runId: string, state: RunState, checkpointRef?: string | null): Promise<void>;
58
93
  appendEvent(event: HarnessEvent): Promise<void>;
59
- listSessions(): Promise<ThreadSummary[]>;
60
- listRuns(): Promise<RunSummary[]>;
94
+ listSessions(filter?: ThreadSummaryFilter): Promise<ThreadSummary[]>;
95
+ listRuns(filter?: RunSummaryFilter): Promise<RunSummary[]>;
61
96
  getRun(runId: string): Promise<RunSummary | null>;
62
97
  getSession(threadId: string): Promise<ThreadSummary | null>;
63
98
  getThreadMeta(threadId: string): Promise<PersistenceThreadMeta | null>;
64
99
  listThreadRuns(threadId: string): Promise<ThreadRunRecord[]>;
65
- listApprovals(): Promise<InternalApprovalRecord[]>;
100
+ listApprovals(filter?: ApprovalFilter): Promise<InternalApprovalRecord[]>;
66
101
  getApproval(approvalId: string): Promise<InternalApprovalRecord | null>;
67
102
  getRunApprovals(threadId: string, runId: string): Promise<InternalApprovalRecord[]>;
68
103
  getRunMeta(threadId: string, runId: string): Promise<PersistenceRunMeta>;
@@ -80,4 +115,30 @@ export interface RuntimePersistence {
80
115
  saveRecoveryIntent(threadId: string, runId: string, intent: RecoveryIntent): Promise<void>;
81
116
  getRecoveryIntent(threadId: string, runId: string): Promise<RecoveryIntent | null>;
82
117
  clearRecoveryIntent(threadId: string, runId: string): Promise<void>;
118
+ enqueueRun(input: {
119
+ threadId: string;
120
+ runId: string;
121
+ priority?: number;
122
+ queueKey?: string | null;
123
+ availableAt?: string;
124
+ }): Promise<void>;
125
+ getQueuedRun(runId: string): Promise<PersistedRunQueueRecord | null>;
126
+ claimQueuedRun(input: {
127
+ threadId: string;
128
+ runId: string;
129
+ workerId: string;
130
+ claimedAt?: string;
131
+ leaseExpiresAt: string;
132
+ }): Promise<PersistedRunQueueRecord>;
133
+ renewRunLease(input: {
134
+ runId: string;
135
+ workerId: string;
136
+ heartbeatAt?: string;
137
+ leaseExpiresAt: string;
138
+ }): Promise<void>;
139
+ releaseRunClaim(runId: string): Promise<void>;
140
+ listExpiredClaimedRuns(cutoffIso: string): Promise<PersistedRunQueueRecord[]>;
141
+ getRunControl(runId: string): Promise<PersistedRunControlRecord | null>;
142
+ requestRunCancel(runId: string, reason?: string): Promise<PersistedRunControlRecord>;
143
+ clearRunCancel(runId: string): Promise<void>;
83
144
  }
@@ -21,7 +21,10 @@ export declare function materializeDeepAgentSkillSourcePaths(options: {
21
21
  }): Promise<string[] | undefined>;
22
22
  export declare class AgentRuntimeAdapter {
23
23
  private readonly options;
24
+ private readonly modelCache;
25
+ private readonly runnableCache;
24
26
  constructor(options?: RuntimeAdapterOptions);
27
+ private getModelCacheKey;
25
28
  private resolveBindingTimeout;
26
29
  private resolveStreamIdleTimeout;
27
30
  private resolveProviderRetryPolicy;
@@ -48,11 +51,13 @@ export declare class AgentRuntimeAdapter {
48
51
  private resolveBuiltinMiddlewareBackend;
49
52
  private invokeBuiltinTaskTool;
50
53
  private resolveBuiltinMiddlewareTools;
54
+ private canReplayToolCallsLocally;
51
55
  private resolveLangChainAutomaticMiddleware;
52
56
  private resolveMiddleware;
53
57
  private resolveCheckpointer;
54
58
  private buildRouteSystemPrompt;
55
59
  private resolveSubagents;
60
+ private createRunnable;
56
61
  create(binding: CompiledAgentBinding): Promise<RunnableLike>;
57
62
  route(input: MessageContent, primaryBinding: CompiledAgentBinding, secondaryBinding: CompiledAgentBinding, options?: {
58
63
  systemPrompt?: string;
@@ -471,9 +471,19 @@ function asStructuredExecutableTool(resolvedTool, modelFacingName, description)
471
471
  }
472
472
  export class AgentRuntimeAdapter {
473
473
  options;
474
+ modelCache = new Map();
475
+ runnableCache = new WeakMap();
474
476
  constructor(options = {}) {
475
477
  this.options = options;
476
478
  }
479
+ getModelCacheKey(model) {
480
+ return JSON.stringify({
481
+ id: model.id,
482
+ runtimeValue: model.runtimeValue,
483
+ init: model.init,
484
+ clientRef: model.clientRef,
485
+ });
486
+ }
477
487
  resolveBindingTimeout(binding) {
478
488
  return resolveTimeoutMs(getBindingModelInit(binding)?.timeout);
479
489
  }
@@ -686,25 +696,40 @@ export class AgentRuntimeAdapter {
686
696
  return sanitizeVisibleText(extractVisibleOutput(synthesized));
687
697
  }
688
698
  async resolveModel(model) {
689
- if (this.options.modelResolver) {
690
- return wrapResolvedModel(await this.options.modelResolver(model.id));
691
- }
692
- if (model.provider === "ollama") {
693
- return wrapResolvedModel(new ChatOllama({ model: model.model, ...model.init }));
694
- }
695
- if (model.provider === "openai-compatible") {
696
- return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...normalizeOpenAICompatibleInit(model.init) }));
697
- }
698
- if (model.provider === "openai") {
699
- return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...model.init }));
699
+ const cacheKey = this.getModelCacheKey(model);
700
+ const cached = this.modelCache.get(cacheKey);
701
+ if (cached) {
702
+ return cached;
700
703
  }
701
- if (model.provider === "anthropic") {
702
- return wrapResolvedModel(new ChatAnthropic({ model: model.model, ...model.init }));
704
+ const pending = (async () => {
705
+ if (this.options.modelResolver) {
706
+ return wrapResolvedModel(await this.options.modelResolver(model.id));
707
+ }
708
+ if (model.provider === "ollama") {
709
+ return wrapResolvedModel(new ChatOllama({ model: model.model, ...model.init }));
710
+ }
711
+ if (model.provider === "openai-compatible") {
712
+ return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...normalizeOpenAICompatibleInit(model.init) }));
713
+ }
714
+ if (model.provider === "openai") {
715
+ return wrapResolvedModel(new ChatOpenAI({ model: model.model, ...model.init }));
716
+ }
717
+ if (model.provider === "anthropic") {
718
+ return wrapResolvedModel(new ChatAnthropic({ model: model.model, ...model.init }));
719
+ }
720
+ if (model.provider === "google" || model.provider === "google-genai" || model.provider === "gemini") {
721
+ return wrapResolvedModel(new ChatGoogle({ model: model.model, ...model.init }));
722
+ }
723
+ return wrapResolvedModel(await initChatModel(model.model, { modelProvider: model.provider, ...model.init }));
724
+ })();
725
+ this.modelCache.set(cacheKey, pending);
726
+ try {
727
+ return await pending;
703
728
  }
704
- if (model.provider === "google" || model.provider === "google-genai" || model.provider === "gemini") {
705
- return wrapResolvedModel(new ChatGoogle({ model: model.model, ...model.init }));
729
+ catch (error) {
730
+ this.modelCache.delete(cacheKey);
731
+ throw error;
706
732
  }
707
- return wrapResolvedModel(await initChatModel(model.model, { modelProvider: model.provider, ...model.init }));
708
733
  }
709
734
  buildToolNameMapping(tools) {
710
735
  return buildToolNameMapping(tools);
@@ -1065,6 +1090,27 @@ export class AgentRuntimeAdapter {
1065
1090
  }
1066
1091
  return tools;
1067
1092
  }
1093
+ canReplayToolCallsLocally(binding, toolCalls, primaryTools, toolNameMapping, executableTools, builtinExecutableTools) {
1094
+ if (toolCalls.length === 0) {
1095
+ return false;
1096
+ }
1097
+ if (isLangChainBinding(binding)) {
1098
+ return true;
1099
+ }
1100
+ return toolCalls.every((toolCall) => {
1101
+ const resolvedToolName = resolveModelFacingToolName(toolCall.name, toolNameMapping, primaryTools);
1102
+ const executable = executableTools.get(toolCall.name) ?? executableTools.get(resolvedToolName);
1103
+ if (executable) {
1104
+ return false;
1105
+ }
1106
+ const builtinExecutable = builtinExecutableTools.get(toolCall.name) ??
1107
+ builtinExecutableTools.get(resolvedToolName) ??
1108
+ createModelFacingToolNameLookupCandidates(toolCall.name)
1109
+ .map((candidate) => builtinExecutableTools.get(candidate))
1110
+ .find((candidate) => candidate !== undefined);
1111
+ return builtinExecutable !== undefined;
1112
+ });
1113
+ }
1068
1114
  async resolveLangChainAutomaticMiddleware(binding) {
1069
1115
  const params = getBindingLangChainParams(binding);
1070
1116
  if (!params) {
@@ -1170,7 +1216,7 @@ export class AgentRuntimeAdapter {
1170
1216
  })),
1171
1217
  })));
1172
1218
  }
1173
- async create(binding) {
1219
+ async createRunnable(binding) {
1174
1220
  if (isLangChainBinding(binding)) {
1175
1221
  const params = getBindingLangChainParams(binding);
1176
1222
  const interruptOn = this.resolveInterruptOn(binding);
@@ -1226,6 +1272,21 @@ export class AgentRuntimeAdapter {
1226
1272
  };
1227
1273
  return createDeepAgent(deepAgentConfig);
1228
1274
  }
1275
+ async create(binding) {
1276
+ const cached = this.runnableCache.get(binding);
1277
+ if (cached) {
1278
+ return cached;
1279
+ }
1280
+ const pending = this.createRunnable(binding);
1281
+ this.runnableCache.set(binding, pending);
1282
+ try {
1283
+ return await pending;
1284
+ }
1285
+ catch (error) {
1286
+ this.runnableCache.delete(binding);
1287
+ throw error;
1288
+ }
1289
+ }
1229
1290
  async route(input, primaryBinding, secondaryBinding, options = {}) {
1230
1291
  const routeModelConfig = getBindingPrimaryModel(primaryBinding) ??
1231
1292
  getBindingPrimaryModel(secondaryBinding);
@@ -1323,12 +1384,17 @@ export class AgentRuntimeAdapter {
1323
1384
  let activeRequest = request;
1324
1385
  let currentMessages = Array.isArray(activeRequest.messages) ? [...activeRequest.messages] : [];
1325
1386
  const maxToolIterations = 8;
1387
+ let pendingResult;
1326
1388
  for (let iteration = 0; iteration < maxToolIterations; iteration += 1) {
1327
- result = await callRuntimeWithToolParseRecovery(activeRequest);
1389
+ result = pendingResult ?? await callRuntimeWithToolParseRecovery(activeRequest);
1390
+ pendingResult = undefined;
1328
1391
  const toolCalls = extractToolCallsFromResult(result);
1329
1392
  if (toolCalls.length === 0) {
1330
1393
  break;
1331
1394
  }
1395
+ if (!this.canReplayToolCallsLocally(binding, toolCalls, primaryTools, toolNameMapping, executableTools, builtinExecutableTools)) {
1396
+ break;
1397
+ }
1332
1398
  if (iteration + 1 === maxToolIterations) {
1333
1399
  throw new Error(`Tool-calling loop exceeded the maximum of ${maxToolIterations} iterations`);
1334
1400
  }
@@ -1377,7 +1443,15 @@ export class AgentRuntimeAdapter {
1377
1443
  const visibleOutput = extractedOutput && !isLikelyToolArgsObject(tryParseJson(extractedOutput)) ? extractedOutput : "";
1378
1444
  const emptyAssistantMessageFailure = extractEmptyAssistantMessageFailure(result);
1379
1445
  const toolFallback = extractToolFallbackContext(result);
1380
- const synthesizedOutput = await this.synthesizeDeepAgentAnswer(binding, input, result);
1446
+ let synthesizedOutput = "";
1447
+ try {
1448
+ synthesizedOutput = await this.synthesizeDeepAgentAnswer(binding, input, result);
1449
+ }
1450
+ catch (error) {
1451
+ if (!(error instanceof RuntimeOperationTimeoutError) || !toolFallback) {
1452
+ throw error;
1453
+ }
1454
+ }
1381
1455
  if (!visibleOutput && !synthesizedOutput && !toolFallback && emptyAssistantMessageFailure) {
1382
1456
  throw new Error(emptyAssistantMessageFailure);
1383
1457
  }
@@ -1,4 +1,4 @@
1
- import type { ApprovalRecord, HarnessEvent, HarnessStreamItem, RuntimeHealthSnapshot, MessageContent, RunRecord, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, RunSummary, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
1
+ import type { ApprovalRecord, CancelOptions, HarnessEvent, HarnessStreamItem, RuntimeHealthSnapshot, MessageContent, RunRecord, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, RunSummary, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
2
2
  import { type ToolMcpServerOptions } from "../mcp.js";
3
3
  import { type InventoryAgentRecord, type InventorySkillRecord } from "./inventory.js";
4
4
  import type { RequirementAssessmentOptions } from "./skill-requirements.js";
@@ -27,6 +27,7 @@ export declare class AgentHarnessRuntime {
27
27
  private readonly healthMonitor;
28
28
  private readonly recoveryConfig;
29
29
  private readonly concurrencyConfig;
30
+ private readonly workerId;
30
31
  private activeRunSlots;
31
32
  private readonly pendingRunSlots;
32
33
  private runtimeEventSequence;
@@ -84,6 +85,9 @@ export declare class AgentHarnessRuntime {
84
85
  private loadPriorHistory;
85
86
  private loadRunInput;
86
87
  private appendAssistantMessage;
88
+ private getRunCancellation;
89
+ private expirePendingApprovals;
90
+ private finalizeCancelledRun;
87
91
  private invokeWithHistory;
88
92
  private buildPersistedRunRequest;
89
93
  private executeQueuedRun;
@@ -100,6 +104,7 @@ export declare class AgentHarnessRuntime {
100
104
  private isDecisionRun;
101
105
  private notifyListener;
102
106
  private acquireRunSlot;
107
+ private dropPendingRunSlot;
103
108
  private dispatchRunListeners;
104
109
  run(options: RunOptions): Promise<RunResult>;
105
110
  streamEvents(options: RunStartOptions): AsyncGenerator<HarnessStreamItem>;
@@ -110,6 +115,9 @@ export declare class AgentHarnessRuntime {
110
115
  }>;
111
116
  close(): Promise<void>;
112
117
  stop(): Promise<void>;
118
+ cancelRun(options: CancelOptions): Promise<RunResult>;
113
119
  private recoverStartupRuns;
120
+ private reclaimExpiredClaimedRuns;
121
+ private isStaleRunningRun;
114
122
  }
115
123
  export { AgentHarnessRuntime as AgentHarness };