@botbotgo/agent-harness 0.0.6 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/api.d.ts CHANGED
@@ -1,22 +1,14 @@
1
- import type { ArtifactListing, RestartConversationOptions, ResumeOptions, RunOptions, RuntimeAdapterOptions, SessionRecord, WorkspaceLoadOptions, WorkspaceBundle } from "./contracts/types.js";
1
+ import type { AgentHarnessHandle, RunOptions, RuntimeAdapterOptions, ThreadRecord, WorkspaceLoadOptions, WorkspaceBundle } from "./contracts/types.js";
2
2
  import { AgentHarness } from "./runtime/harness.js";
3
- export declare function loadWorkspaceBundle(workspaceRoot: string, options?: WorkspaceLoadOptions): Promise<WorkspaceBundle>;
4
- export declare function createHarness(workspace: WorkspaceBundle, adapterOptions?: RuntimeAdapterOptions): AgentHarness;
5
- export declare function listHarnessSessions(harness: AgentHarness, filter?: {
6
- agentId?: string;
7
- }): Promise<SessionRecord[]>;
8
- export declare function getHarnessApproval(harness: AgentHarness, approvalId: string): Promise<import("./contracts/types.js").ApprovalRecord | null>;
9
- export declare function runHarness(harness: AgentHarness, options: RunOptions): Promise<import("./contracts/types.js").RunResult>;
10
- export declare function streamHarness(harness: AgentHarness, options: RunOptions): Promise<AsyncGenerator<string, any, any>>;
11
- export declare function streamHarnessEvents(harness: AgentHarness, options: RunOptions): Promise<AsyncGenerator<import("./contracts/types.js").HarnessStreamItem, any, any>>;
12
- export declare function resumeHarness(harness: AgentHarness, options: ResumeOptions): Promise<import("./contracts/types.js").RunResult>;
13
- export declare function submitHarnessDecision(harness: AgentHarness, options: ResumeOptions): Promise<import("./contracts/types.js").RunResult>;
14
- export declare function approveHarnessRun(harness: AgentHarness, threadId: string, runId?: string): Promise<import("./contracts/types.js").RunResult>;
15
- export declare function rejectHarnessRun(harness: AgentHarness, threadId: string, runId?: string): Promise<import("./contracts/types.js").RunResult>;
16
- export declare function restartHarnessConversation(harness: AgentHarness, options: RestartConversationOptions): Promise<import("./contracts/types.js").RunResult & {
17
- restart: Record<string, string>;
18
- }>;
19
- export declare function listHarnessArtifacts(harness: AgentHarness, threadId: string, runId?: string): Promise<ArtifactListing>;
20
- export declare function subscribeHarness(harness: AgentHarness, listener: Parameters<AgentHarness["subscribe"]>[0]): () => void;
21
- export declare function closeHarness(harness: AgentHarness): Promise<void>;
22
- export { loadWorkspaceBundle as loadWorkspaceFacade, listHarnessSessions as listSessionsFacade, getHarnessApproval as getApprovalFacade, runHarness as runFacade, streamHarness as streamFacade, streamHarnessEvents as streamEventsFacade, resumeHarness as resumeFacade, submitHarnessDecision as submitDecisionFacade, approveHarnessRun as approveFacade, rejectHarnessRun as rejectFacade, restartHarnessConversation as restartConversationFacade, listHarnessArtifacts as artifactsFacade, subscribeHarness as subscribeFacade, closeHarness as closeFacade, };
3
+ type CreateAgentHarnessOptions = {
4
+ load?: WorkspaceLoadOptions;
5
+ adapter?: RuntimeAdapterOptions;
6
+ };
7
+ export declare function createAgentHarness(): Promise<AgentHarnessHandle>;
8
+ export declare function createAgentHarness(workspaceRoot: string, options?: CreateAgentHarnessOptions): Promise<AgentHarnessHandle>;
9
+ export declare function createAgentHarness(workspace: WorkspaceBundle, options?: CreateAgentHarnessOptions): Promise<AgentHarnessHandle>;
10
+ export declare function run(harness: AgentHarnessHandle, options: RunOptions): Promise<import("./contracts/types.js").RunResult>;
11
+ export declare function subscribe(harness: AgentHarnessHandle, listener: Parameters<AgentHarness["subscribe"]>[0]): () => void;
12
+ export declare function getThread(harness: AgentHarnessHandle, threadId: string): Promise<ThreadRecord | null>;
13
+ export declare function stop(harness: AgentHarnessHandle): Promise<void>;
14
+ export {};
package/dist/api.js CHANGED
@@ -1,48 +1,47 @@
1
1
  import { AgentHarness } from "./runtime/harness.js";
2
2
  import { loadWorkspace } from "./workspace/compile.js";
3
- export async function loadWorkspaceBundle(workspaceRoot, options = {}) {
4
- return loadWorkspace(workspaceRoot, options);
3
+ const HARNESS_INSTANCE = Symbol.for("@botbotgo/agent-harness/instance");
4
+ function registerHarness(harness) {
5
+ const handle = { kind: "AgentHarnessHandle" };
6
+ Object.defineProperty(handle, HARNESS_INSTANCE, {
7
+ value: harness,
8
+ enumerable: false,
9
+ configurable: false,
10
+ writable: false,
11
+ });
12
+ return Object.freeze(handle);
13
+ }
14
+ function requireHarness(handle) {
15
+ if (handle instanceof AgentHarness) {
16
+ return handle;
17
+ }
18
+ if (typeof handle === "object" && handle !== null && "run" in handle && "subscribe" in handle && "close" in handle) {
19
+ return handle;
20
+ }
21
+ const harness = handle[HARNESS_INSTANCE];
22
+ if (!harness) {
23
+ throw new Error("Unknown or stopped AgentHarnessHandle");
24
+ }
25
+ return harness;
26
+ }
27
+ export async function createAgentHarness(input = process.cwd(), options = {}) {
28
+ const workspace = typeof input === "string"
29
+ ? await loadWorkspace(input, options.load ?? {})
30
+ : input;
31
+ const harness = new AgentHarness(workspace, options.adapter ?? {});
32
+ await harness.initialize();
33
+ return registerHarness(harness);
34
+ }
35
+ export async function run(harness, options) {
36
+ return requireHarness(harness).run(options);
37
+ }
38
+ export function subscribe(harness, listener) {
39
+ return requireHarness(harness).subscribe(listener);
40
+ }
41
+ export async function getThread(harness, threadId) {
42
+ return requireHarness(harness).getThread(threadId);
43
+ }
44
+ export async function stop(harness) {
45
+ const instance = requireHarness(harness);
46
+ return instance.close();
5
47
  }
6
- export function createHarness(workspace, adapterOptions = {}) {
7
- return new AgentHarness(workspace, adapterOptions);
8
- }
9
- export async function listHarnessSessions(harness, filter) {
10
- return harness.listSessions(filter);
11
- }
12
- export async function getHarnessApproval(harness, approvalId) {
13
- return harness.getApproval(approvalId);
14
- }
15
- export async function runHarness(harness, options) {
16
- return harness.run(options);
17
- }
18
- export async function streamHarness(harness, options) {
19
- return harness.stream(options);
20
- }
21
- export async function streamHarnessEvents(harness, options) {
22
- return harness.streamEvents(options);
23
- }
24
- export async function resumeHarness(harness, options) {
25
- return harness.resume(options);
26
- }
27
- export async function submitHarnessDecision(harness, options) {
28
- return harness.submitDecision(options);
29
- }
30
- export async function approveHarnessRun(harness, threadId, runId) {
31
- return harness.approve(threadId, runId);
32
- }
33
- export async function rejectHarnessRun(harness, threadId, runId) {
34
- return harness.reject(threadId, runId);
35
- }
36
- export async function restartHarnessConversation(harness, options) {
37
- return harness.restartConversation(options);
38
- }
39
- export async function listHarnessArtifacts(harness, threadId, runId) {
40
- return harness.artifacts(threadId, runId);
41
- }
42
- export function subscribeHarness(harness, listener) {
43
- return harness.subscribe(listener);
44
- }
45
- export async function closeHarness(harness) {
46
- return harness.close();
47
- }
48
- export { loadWorkspaceBundle as loadWorkspaceFacade, listHarnessSessions as listSessionsFacade, getHarnessApproval as getApprovalFacade, runHarness as runFacade, streamHarness as streamFacade, streamHarnessEvents as streamEventsFacade, resumeHarness as resumeFacade, submitHarnessDecision as submitDecisionFacade, approveHarnessRun as approveFacade, rejectHarnessRun as rejectFacade, restartHarnessConversation as restartConversationFacade, listHarnessArtifacts as artifactsFacade, subscribeHarness as subscribeFacade, closeHarness as closeFacade, };
@@ -171,7 +171,7 @@ export type WorkspaceLoadOptions = {
171
171
  overlayRoots?: string[];
172
172
  builtinSources?: string[];
173
173
  };
174
- export type SessionRecord = {
174
+ export type ThreadSummary = {
175
175
  agentId: string;
176
176
  threadId: string;
177
177
  latestRunId: string;
@@ -179,6 +179,7 @@ export type SessionRecord = {
179
179
  updatedAt: string;
180
180
  status: RunState;
181
181
  };
182
+ export type SessionRecord = ThreadSummary;
182
183
  export type HarnessEvent = {
183
184
  eventId: string;
184
185
  eventType: string;
@@ -200,11 +201,31 @@ export type RunResult = {
200
201
  pendingActionId?: string;
201
202
  delegationId?: string;
202
203
  };
203
- export type RunOptions = {
204
+ export type RunListeners = {
205
+ onChunk?: (chunk: string) => void | Promise<void>;
206
+ onEvent?: (event: HarnessEvent) => void | Promise<void>;
207
+ onReasoning?: (chunk: string) => void | Promise<void>;
208
+ onStep?: (step: string) => void | Promise<void>;
209
+ onToolResult?: (item: {
210
+ toolName: string;
211
+ output: unknown;
212
+ }) => void | Promise<void>;
213
+ };
214
+ export type RunStartOptions = {
204
215
  agentId?: string;
205
216
  input: string;
206
217
  threadId?: string;
218
+ listeners?: RunListeners;
207
219
  };
220
+ export type RunDecisionOptions = {
221
+ threadId: string;
222
+ runId?: string;
223
+ approvalId?: string;
224
+ decision: "approve" | "edit" | "reject";
225
+ editedInput?: Record<string, unknown>;
226
+ listeners?: RunListeners;
227
+ };
228
+ export type RunOptions = RunStartOptions | RunDecisionOptions;
208
229
  export type HarnessStreamItem = {
209
230
  type: "event";
210
231
  event: HarnessEvent;
@@ -240,6 +261,33 @@ export type TranscriptMessage = {
240
261
  runId: string;
241
262
  createdAt: string;
242
263
  };
264
+ export type ThreadRunRecord = {
265
+ runId: string;
266
+ agentId: string;
267
+ executionMode: string;
268
+ createdAt: string;
269
+ updatedAt: string;
270
+ state: RunState;
271
+ checkpointRef: string | null;
272
+ resumable: boolean;
273
+ };
274
+ export type ThreadRecord = {
275
+ threadId: string;
276
+ entryAgentId: string;
277
+ currentState: RunState;
278
+ latestRunId: string;
279
+ createdAt: string;
280
+ updatedAt: string;
281
+ messages: TranscriptMessage[];
282
+ runs: ThreadRunRecord[];
283
+ pendingDecision?: {
284
+ approvalId: string;
285
+ pendingActionId: string;
286
+ toolName: string;
287
+ allowedDecisions: Array<"approve" | "edit" | "reject">;
288
+ requestedAt: string;
289
+ };
290
+ };
243
291
  export type ResumeOptions = {
244
292
  threadId?: string;
245
293
  runId?: string;
@@ -357,3 +405,6 @@ export type EventSubscriber = {
357
405
  kind: string;
358
406
  onEvent: (event: HarnessEvent) => void | Promise<void>;
359
407
  };
408
+ export type AgentHarnessHandle = {
409
+ readonly kind: "AgentHarnessHandle";
410
+ };
package/dist/index.d.ts CHANGED
@@ -1,7 +1 @@
1
- export * from "./contracts/types.js";
2
- export * from "./extensions.js";
3
- export * from "./api.js";
4
- export * from "./presentation.js";
5
- export * from "./runtime/index.js";
6
- export * from "./workspace/index.js";
7
- export { createBuiltinBackendResolver, createBuiltinToolResolver } from "./vendor/builtins.js";
1
+ export { createAgentHarness, getThread, run, subscribe, stop } from "./api.js";
package/dist/index.js CHANGED
@@ -1,7 +1 @@
1
- export * from "./contracts/types.js";
2
- export * from "./extensions.js";
3
- export * from "./api.js";
4
- export * from "./presentation.js";
5
- export * from "./runtime/index.js";
6
- export * from "./workspace/index.js";
7
- export { createBuiltinBackendResolver, createBuiltinToolResolver } from "./vendor/builtins.js";
1
+ export { createAgentHarness, getThread, run, subscribe, stop } from "./api.js";
@@ -1,4 +1,13 @@
1
- import type { ApprovalRecord, ArtifactListing, ArtifactRecord, DelegationRecord, HarnessEvent, RunState, SessionRecord, TranscriptMessage } from "../contracts/types.js";
1
+ import type { ApprovalRecord, ArtifactListing, ArtifactRecord, DelegationRecord, HarnessEvent, RunState, ThreadSummary, ThreadRunRecord, TranscriptMessage } from "../contracts/types.js";
2
+ type ThreadMeta = {
3
+ threadId: string;
4
+ workspaceId: string;
5
+ entryAgentId: string;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ status: RunState;
9
+ latestRunId: string;
10
+ };
2
11
  export declare class FilePersistence {
3
12
  private readonly runRoot;
4
13
  constructor(runRoot: string);
@@ -21,8 +30,10 @@ export declare class FilePersistence {
21
30
  }): Promise<void>;
22
31
  setRunState(threadId: string, runId: string, state: RunState, checkpointRef?: string | null): Promise<void>;
23
32
  appendEvent(event: HarnessEvent): Promise<void>;
24
- listSessions(): Promise<SessionRecord[]>;
25
- getSession(threadId: string): Promise<SessionRecord | null>;
33
+ listSessions(): Promise<ThreadSummary[]>;
34
+ getSession(threadId: string): Promise<ThreadSummary | null>;
35
+ getThreadMeta(threadId: string): Promise<ThreadMeta | null>;
36
+ listThreadRuns(threadId: string): Promise<ThreadRunRecord[]>;
26
37
  listRunEvents(threadId: string, runId: string): Promise<HarnessEvent[]>;
27
38
  listApprovals(): Promise<ApprovalRecord[]>;
28
39
  getApproval(approvalId: string): Promise<ApprovalRecord | null>;
@@ -37,3 +48,4 @@ export declare class FilePersistence {
37
48
  appendThreadMessage(threadId: string, message: TranscriptMessage): Promise<void>;
38
49
  listThreadMessages(threadId: string, limit?: number): Promise<TranscriptMessage[]>;
39
50
  }
51
+ export {};
@@ -94,7 +94,11 @@ export class FilePersistence {
94
94
  }
95
95
  async setRunState(threadId, runId, state, checkpointRef) {
96
96
  const lifecyclePath = path.join(this.runDir(threadId, runId), "lifecycle.json");
97
- const lifecycle = await readJson(lifecyclePath);
97
+ const runMetaPath = path.join(this.runDir(threadId, runId), "meta.json");
98
+ const [lifecycle, runMeta] = await Promise.all([
99
+ readJson(lifecyclePath),
100
+ readJson(runMetaPath),
101
+ ]);
98
102
  const now = new Date().toISOString();
99
103
  const next = {
100
104
  state,
@@ -104,7 +108,13 @@ export class FilePersistence {
104
108
  resumable: state === "waiting_for_approval",
105
109
  checkpointRef: checkpointRef ?? lifecycle.checkpointRef,
106
110
  };
107
- await writeJson(lifecyclePath, next);
111
+ await Promise.all([
112
+ writeJson(lifecyclePath, next),
113
+ writeJson(runMetaPath, {
114
+ ...runMeta,
115
+ updatedAt: now,
116
+ }),
117
+ ]);
108
118
  if (checkpointRef !== undefined) {
109
119
  await writeJson(path.join(this.runDir(threadId, runId), "checkpoint-ref.json"), {
110
120
  threadId,
@@ -176,6 +186,38 @@ export class FilePersistence {
176
186
  status: index.status,
177
187
  };
178
188
  }
189
+ async getThreadMeta(threadId) {
190
+ const filePath = path.join(this.threadDir(threadId), "meta.json");
191
+ if (!(await fileExists(filePath))) {
192
+ return null;
193
+ }
194
+ return readJson(filePath);
195
+ }
196
+ async listThreadRuns(threadId) {
197
+ const runsDir = path.join(this.threadDir(threadId), "runs");
198
+ if (!(await fileExists(runsDir))) {
199
+ return [];
200
+ }
201
+ const runIds = (await readdir(runsDir)).sort();
202
+ const runs = await Promise.all(runIds.map(async (runId) => {
203
+ const runDir = this.runDir(threadId, runId);
204
+ const [meta, lifecycle] = await Promise.all([
205
+ readJson(path.join(runDir, "meta.json")),
206
+ readJson(path.join(runDir, "lifecycle.json")),
207
+ ]);
208
+ return {
209
+ runId: meta.runId,
210
+ agentId: meta.agentId,
211
+ executionMode: meta.executionMode,
212
+ createdAt: meta.createdAt,
213
+ updatedAt: meta.updatedAt,
214
+ state: lifecycle.state,
215
+ checkpointRef: lifecycle.checkpointRef,
216
+ resumable: lifecycle.resumable,
217
+ };
218
+ }));
219
+ return runs.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
220
+ }
179
221
  async listRunEvents(threadId, runId) {
180
222
  const eventsDir = path.join(this.runDir(threadId, runId), "events");
181
223
  if (!(await fileExists(eventsDir))) {
@@ -1,5 +1,4 @@
1
- import { Readable } from "node:stream";
2
- import type { ApprovalRecord, ArtifactListing, DelegationRecord, HarnessEvent, HarnessStreamItem, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, SessionRecord, WorkspaceBundle } from "../contracts/types.js";
1
+ import type { ApprovalRecord, HarnessEvent, HarnessStreamItem, RunStartOptions, RestartConversationOptions, RuntimeAdapterOptions, ResumeOptions, RunOptions, RunResult, ThreadSummary, ThreadRecord, WorkspaceBundle } from "../contracts/types.js";
3
2
  export declare class AgentHarness {
4
3
  private readonly workspace;
5
4
  private readonly runtimeAdapterOptions;
@@ -25,31 +24,25 @@ export declare class AgentHarness {
25
24
  private resolveVectorStore;
26
25
  constructor(workspace: WorkspaceBundle, runtimeAdapterOptions?: RuntimeAdapterOptions);
27
26
  initialize(): Promise<void>;
28
- subscribeEvents(listener: (event: HarnessEvent) => void): () => void;
29
27
  subscribe(listener: (event: HarnessEvent) => void): () => void;
30
28
  listSessions(filter?: {
31
29
  agentId?: string;
32
- }): Promise<SessionRecord[]>;
33
- getSession(threadId: string): Promise<SessionRecord | null>;
34
- getEvents(threadId: string, runId?: string): Promise<HarnessEvent[]>;
30
+ }): Promise<ThreadSummary[]>;
31
+ private getSession;
32
+ getThread(threadId: string): Promise<ThreadRecord | null>;
35
33
  listPendingApprovals(): Promise<ApprovalRecord[]>;
36
- getApproval(approvalId: string): Promise<ApprovalRecord | null>;
37
- listDelegations(): Promise<DelegationRecord[]>;
38
34
  routeAgent(input: string, options?: {
39
35
  threadId?: string;
40
36
  }): Promise<string>;
41
- artifacts(threadId: string, runId?: string): Promise<ArtifactListing>;
42
37
  private emit;
43
38
  private persistApproval;
44
39
  private resolveApprovalRecord;
40
+ private isDecisionRun;
41
+ private notifyListener;
42
+ private dispatchRunListeners;
45
43
  run(options: RunOptions): Promise<RunResult>;
46
- stream(options: RunOptions): AsyncGenerator<string>;
47
- streamEvents(options: RunOptions): AsyncGenerator<HarnessStreamItem>;
48
- streamReadable(options: RunOptions): Promise<Readable>;
44
+ streamEvents(options: RunStartOptions): AsyncGenerator<HarnessStreamItem>;
49
45
  resume(options: ResumeOptions): Promise<RunResult>;
50
- submitDecision(options: ResumeOptions): Promise<RunResult>;
51
- approve(threadId: string, runId?: string): Promise<RunResult>;
52
- reject(threadId: string, runId?: string): Promise<RunResult>;
53
46
  restartConversation(options: RestartConversationOptions): Promise<RunResult & {
54
47
  restart: Record<string, string>;
55
48
  }>;
@@ -1,4 +1,3 @@
1
- import { Readable } from "node:stream";
2
1
  import { AUTO_AGENT_ID } from "../contracts/types.js";
3
2
  import { FilePersistence } from "../persistence/file-store.js";
4
3
  import { createPersistentId } from "../utils/id.js";
@@ -147,38 +146,57 @@ export class AgentHarness {
147
146
  async initialize() {
148
147
  await this.persistence.initialize();
149
148
  }
150
- subscribeEvents(listener) {
151
- return this.eventBus.subscribe(listener);
152
- }
153
149
  subscribe(listener) {
154
- return this.subscribeEvents(listener);
150
+ return this.eventBus.subscribe(listener);
155
151
  }
156
152
  async listSessions(filter) {
157
- const sessions = await this.persistence.listSessions();
153
+ const threadSummaries = await this.persistence.listSessions();
158
154
  if (!filter?.agentId) {
159
- return sessions;
155
+ return threadSummaries;
160
156
  }
161
- return sessions.filter((session) => session.agentId === filter.agentId);
157
+ return threadSummaries.filter((thread) => thread.agentId === filter.agentId);
162
158
  }
163
159
  async getSession(threadId) {
164
160
  return this.persistence.getSession(threadId);
165
161
  }
166
- async getEvents(threadId, runId) {
167
- const session = await this.getSession(threadId);
168
- if (!session) {
169
- throw new Error(`Unknown thread ${threadId}`);
170
- }
171
- return this.persistence.listRunEvents(threadId, runId ?? session.latestRunId);
162
+ async getThread(threadId) {
163
+ const [threadSummary, meta, messages, runs] = await Promise.all([
164
+ this.getSession(threadId),
165
+ this.persistence.getThreadMeta(threadId),
166
+ this.persistence.listThreadMessages(threadId, 200),
167
+ this.persistence.listThreadRuns(threadId),
168
+ ]);
169
+ if (!threadSummary || !meta) {
170
+ return null;
171
+ }
172
+ const latestRunId = threadSummary.latestRunId;
173
+ const latestApprovals = await this.persistence.getRunApprovals(threadId, latestRunId);
174
+ const pendingApproval = latestApprovals
175
+ .filter((approval) => approval.status === "pending")
176
+ .sort((left, right) => right.requestedAt.localeCompare(left.requestedAt))[0];
177
+ return {
178
+ threadId,
179
+ entryAgentId: meta.entryAgentId,
180
+ currentState: threadSummary.status,
181
+ latestRunId,
182
+ createdAt: meta.createdAt,
183
+ updatedAt: threadSummary.updatedAt,
184
+ messages,
185
+ runs,
186
+ pendingDecision: pendingApproval
187
+ ? {
188
+ approvalId: pendingApproval.approvalId,
189
+ pendingActionId: pendingApproval.pendingActionId,
190
+ toolName: pendingApproval.toolName,
191
+ allowedDecisions: pendingApproval.allowedDecisions,
192
+ requestedAt: pendingApproval.requestedAt,
193
+ }
194
+ : undefined,
195
+ };
172
196
  }
173
197
  async listPendingApprovals() {
174
198
  return (await this.persistence.listApprovals()).filter((approval) => approval.status === "pending");
175
199
  }
176
- async getApproval(approvalId) {
177
- return this.persistence.getApproval(approvalId);
178
- }
179
- async listDelegations() {
180
- return this.persistence.listDelegations();
181
- }
182
200
  async routeAgent(input, options = {}) {
183
201
  const routingInput = await this.buildRoutingInput(input, options.threadId);
184
202
  const normalized = input.trim().toLowerCase();
@@ -210,13 +228,6 @@ export class AgentHarness {
210
228
  return heuristicRoute(input, primaryBinding, secondaryBinding);
211
229
  }
212
230
  }
213
- async artifacts(threadId, runId) {
214
- const session = await this.getSession(threadId);
215
- if (!session) {
216
- throw new Error(`Unknown thread ${threadId}`);
217
- }
218
- return this.persistence.listArtifacts(threadId, runId ?? session.latestRunId);
219
- }
220
231
  async emit(threadId, runId, sequence, eventType, payload, source = "runtime") {
221
232
  const event = createHarnessEvent(threadId, runId, sequence, eventType, payload, source);
222
233
  await this.persistence.appendEvent(event);
@@ -239,7 +250,7 @@ export class AgentHarness {
239
250
  });
240
251
  return approval;
241
252
  }
242
- async resolveApprovalRecord(options, session) {
253
+ async resolveApprovalRecord(options, thread) {
243
254
  if (options.approvalId) {
244
255
  const approval = await this.persistence.getApproval(options.approvalId);
245
256
  if (!approval) {
@@ -247,8 +258,8 @@ export class AgentHarness {
247
258
  }
248
259
  return approval;
249
260
  }
250
- const runId = options.runId ?? session.latestRunId;
251
- const approvals = await this.persistence.getRunApprovals(options.threadId ?? session.threadId, runId);
261
+ const runId = options.runId ?? thread.latestRunId;
262
+ const approvals = await this.persistence.getRunApprovals(options.threadId ?? thread.threadId, runId);
252
263
  const approval = approvals
253
264
  .filter((candidate) => candidate.status === "pending")
254
265
  .sort((left, right) => right.requestedAt.localeCompare(left.requestedAt))[0];
@@ -257,7 +268,75 @@ export class AgentHarness {
257
268
  }
258
269
  return approval;
259
270
  }
271
+ isDecisionRun(options) {
272
+ return "decision" in options;
273
+ }
274
+ async notifyListener(listener, value) {
275
+ if (!listener) {
276
+ return;
277
+ }
278
+ await listener(value);
279
+ }
280
+ async dispatchRunListeners(stream, listeners) {
281
+ let latestEvent;
282
+ let output = "";
283
+ for await (const item of stream) {
284
+ if (item.type === "event") {
285
+ latestEvent = item.event;
286
+ await this.notifyListener(listeners.onEvent, item.event);
287
+ continue;
288
+ }
289
+ if (item.type === "content") {
290
+ output += item.content;
291
+ await this.notifyListener(listeners.onChunk, item.content);
292
+ continue;
293
+ }
294
+ if (item.type === "reasoning") {
295
+ await this.notifyListener(listeners.onReasoning, item.content);
296
+ continue;
297
+ }
298
+ if (item.type === "step") {
299
+ await this.notifyListener(listeners.onStep, item.content);
300
+ continue;
301
+ }
302
+ if (item.type === "tool-result") {
303
+ await this.notifyListener(listeners.onToolResult, {
304
+ toolName: item.toolName,
305
+ output: item.output,
306
+ });
307
+ }
308
+ }
309
+ if (!latestEvent) {
310
+ throw new Error("run did not emit any events");
311
+ }
312
+ const thread = await this.getThread(latestEvent.threadId);
313
+ if (!thread) {
314
+ throw new Error(`Unknown thread ${latestEvent.threadId}`);
315
+ }
316
+ return {
317
+ threadId: thread.threadId,
318
+ runId: thread.latestRunId,
319
+ agentId: thread.runs[0]?.agentId ?? thread.entryAgentId,
320
+ state: thread.currentState,
321
+ output,
322
+ approvalId: thread.pendingDecision?.approvalId,
323
+ pendingActionId: thread.pendingDecision?.pendingActionId,
324
+ };
325
+ }
260
326
  async run(options) {
327
+ if (this.isDecisionRun(options)) {
328
+ const resumeOptions = {
329
+ threadId: options.threadId,
330
+ runId: options.runId,
331
+ approvalId: options.approvalId,
332
+ decision: options.decision,
333
+ editedInput: options.editedInput,
334
+ };
335
+ return this.resume(resumeOptions);
336
+ }
337
+ if (options.listeners) {
338
+ return this.dispatchRunListeners(this.streamEvents(options), options.listeners);
339
+ }
261
340
  const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
262
341
  const binding = this.workspace.bindings.get(selectedAgentId);
263
342
  if (!binding) {
@@ -359,13 +438,6 @@ export class AgentHarness {
359
438
  };
360
439
  }
361
440
  }
362
- async *stream(options) {
363
- for await (const item of this.streamEvents(options)) {
364
- if (item.type === "content") {
365
- yield item.content;
366
- }
367
- }
368
- }
369
441
  async *streamEvents(options) {
370
442
  const selectedAgentId = await this.resolveSelectedAgentId(options.input, options.agentId, options.threadId);
371
443
  const binding = this.workspace.bindings.get(selectedAgentId);
@@ -592,25 +664,22 @@ export class AgentHarness {
592
664
  }
593
665
  }
594
666
  }
595
- async streamReadable(options) {
596
- return Readable.from(this.stream(options));
597
- }
598
667
  async resume(options) {
599
668
  const approvalById = options.approvalId ? await this.persistence.getApproval(options.approvalId) : null;
600
- const session = options.threadId
669
+ const thread = options.threadId
601
670
  ? await this.getSession(options.threadId)
602
671
  : approvalById
603
672
  ? await this.getSession(approvalById.threadId)
604
673
  : null;
605
- if (!session) {
674
+ if (!thread) {
606
675
  throw new Error("resume requires either threadId or approvalId");
607
676
  }
608
- const approval = approvalById ?? await this.resolveApprovalRecord(options, session);
677
+ const approval = approvalById ?? await this.resolveApprovalRecord(options, thread);
609
678
  const threadId = approval.threadId;
610
679
  const runId = approval.runId;
611
- const binding = this.workspace.bindings.get(session.agentId);
680
+ const binding = this.workspace.bindings.get(thread.agentId);
612
681
  if (!binding) {
613
- throw new Error(`Unknown agent ${session.agentId}`);
682
+ throw new Error(`Unknown agent ${thread.agentId}`);
614
683
  }
615
684
  await this.persistence.setRunState(threadId, runId, "resuming", `checkpoints/${threadId}/${runId}/cp-1`);
616
685
  await this.emit(threadId, runId, 5, "run.resumed", {
@@ -655,24 +724,15 @@ export class AgentHarness {
655
724
  pendingActionId: approval.pendingActionId,
656
725
  };
657
726
  }
658
- async submitDecision(options) {
659
- return this.resume(options);
660
- }
661
- async approve(threadId, runId) {
662
- return this.submitDecision({ threadId, runId, decision: "approve" });
663
- }
664
- async reject(threadId, runId) {
665
- return this.submitDecision({ threadId, runId, decision: "reject" });
666
- }
667
727
  async restartConversation(options) {
668
- const session = await this.getSession(options.threadId);
669
- if (!session) {
728
+ const thread = await this.getSession(options.threadId);
729
+ if (!thread) {
670
730
  throw new Error(`Unknown thread ${options.threadId}`);
671
731
  }
672
- const sourceRunId = session.latestRunId;
732
+ const sourceRunId = thread.latestRunId;
673
733
  const targetThreadId = options.mode === "restart-new-thread" ? createPersistentId() : options.threadId;
674
734
  const result = await this.run({
675
- agentId: session.agentId,
735
+ agentId: thread.agentId,
676
736
  input: options.input,
677
737
  threadId: options.mode === "restart-new-thread" ? undefined : targetThreadId,
678
738
  });
@@ -5,17 +5,17 @@ function excerpt(message) {
5
5
  const normalized = message.content.replace(/\s+/g, " ").trim();
6
6
  return normalized.length > 240 ? `${normalized.slice(0, 237)}...` : normalized;
7
7
  }
8
- function renderStatusMarkdown(session, messages) {
8
+ function renderStatusMarkdown(thread, messages) {
9
9
  const userMessages = messages.filter((message) => message.role === "user");
10
10
  const assistantMessages = messages.filter((message) => message.role === "assistant");
11
11
  return [
12
12
  "# Thread Status",
13
13
  "",
14
- `- thread_id: ${session.threadId}`,
15
- `- latest_run_id: ${session.latestRunId}`,
16
- `- agent_id: ${session.agentId}`,
17
- `- status: ${session.status}`,
18
- `- updated_at: ${session.updatedAt}`,
14
+ `- thread_id: ${thread.threadId}`,
15
+ `- latest_run_id: ${thread.latestRunId}`,
16
+ `- agent_id: ${thread.agentId}`,
17
+ `- status: ${thread.status}`,
18
+ `- updated_at: ${thread.updatedAt}`,
19
19
  "",
20
20
  "## Recent User Message",
21
21
  excerpt(userMessages.at(-1)),
@@ -64,8 +64,8 @@ export class ThreadMemorySync {
64
64
  await task;
65
65
  }
66
66
  async syncThread(threadId) {
67
- const session = await this.persistence.getSession(threadId);
68
- if (!session) {
67
+ const thread = await this.persistence.getSession(threadId);
68
+ if (!thread) {
69
69
  return;
70
70
  }
71
71
  const [messages, approvals] = await Promise.all([
@@ -77,7 +77,7 @@ export class ThreadMemorySync {
77
77
  return;
78
78
  }
79
79
  await Promise.all([
80
- this.store.put(["memories", "threads", threadId], "status.md", { content: `${renderStatusMarkdown(session, messages)}\n` }),
80
+ this.store.put(["memories", "threads", threadId], "status.md", { content: `${renderStatusMarkdown(thread, messages)}\n` }),
81
81
  this.store.put(["memories", "threads", threadId], "open-approvals.md", { content: `${renderOpenApprovalsMarkdown(pendingApprovals)}\n` }),
82
82
  ]);
83
83
  }
@@ -10,13 +10,41 @@ function resolveBuiltinPath(kind, ref) {
10
10
  throw new Error(`Unsupported builtin skill discovery path ${ref}`);
11
11
  }
12
12
  const suffix = normalized.replace(/^skills\/?/, "");
13
- return suffix ? path.join(builtinSkillsRoot(), suffix) : builtinSkillsRoot();
13
+ const root = builtinSkillsRoot();
14
+ if (!suffix) {
15
+ return root;
16
+ }
17
+ const candidate = path.join(root, suffix);
18
+ if (existsSync(candidate)) {
19
+ return candidate;
20
+ }
21
+ const sourceFallback = root.includes(`${path.sep}dist${path.sep}skills`)
22
+ ? path.join(path.resolve(root, "..", ".."), "skills", suffix)
23
+ : null;
24
+ if (sourceFallback && existsSync(sourceFallback)) {
25
+ return sourceFallback;
26
+ }
27
+ return candidate;
14
28
  }
15
29
  if (!normalized.startsWith("agents")) {
16
30
  throw new Error(`Unsupported builtin subagent discovery path ${ref}`);
17
31
  }
18
32
  const suffix = normalized.replace(/^agents\/?/, "");
19
- return suffix ? path.join(builtinConfigRoot(), "agents", suffix) : path.join(builtinConfigRoot(), "agents");
33
+ const root = path.join(builtinConfigRoot(), "agents");
34
+ if (!suffix) {
35
+ return root;
36
+ }
37
+ const candidate = path.join(root, suffix);
38
+ if (existsSync(candidate)) {
39
+ return candidate;
40
+ }
41
+ const sourceFallback = root.includes(`${path.sep}dist${path.sep}config${path.sep}agents`)
42
+ ? path.join(path.resolve(root, "..", "..", ".."), "config", "agents", suffix)
43
+ : null;
44
+ if (sourceFallback && existsSync(sourceFallback)) {
45
+ return sourceFallback;
46
+ }
47
+ return candidate;
20
48
  }
21
49
  function preferPackageConvention(root, kind) {
22
50
  const candidates = kind === "skills"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "Agent Harness framework package",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -25,7 +25,7 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@botbotgo/agent-harness-builtin": "0.0.6"
28
+ "@botbotgo/agent-harness-builtin": "0.0.7"
29
29
  },
30
30
  "scripts": {
31
31
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && cp -R config dist/",