@poncho-ai/harness 0.40.1 → 0.42.0

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/index.js CHANGED
@@ -5911,6 +5911,11 @@ var McpHttpError = class extends Error {
5911
5911
  this.status = status;
5912
5912
  }
5913
5913
  };
5914
+ var McpSessionExpiredError = class extends Error {
5915
+ constructor() {
5916
+ super("MCP session expired");
5917
+ }
5918
+ };
5914
5919
  var StreamableHttpMcpRpcClient = class {
5915
5920
  endpoint;
5916
5921
  timeoutMs;
@@ -5962,6 +5967,9 @@ var StreamableHttpMcpRpcClient = class {
5962
5967
  if (response.status === 403) {
5963
5968
  throw new McpHttpError(403, "MCP server forbidden");
5964
5969
  }
5970
+ if (response.status === 404 && this.sessionId) {
5971
+ throw new McpSessionExpiredError();
5972
+ }
5965
5973
  if (!response.ok) {
5966
5974
  throw new Error(`MCP HTTP request failed with status ${response.status}`);
5967
5975
  }
@@ -6036,20 +6044,32 @@ var StreamableHttpMcpRpcClient = class {
6036
6044
  throw new Error(`MCP response missing JSON-RPC payload for id ${id}`);
6037
6045
  }
6038
6046
  async request(method, params) {
6039
- await this.ensureInitialized();
6040
- const id = this.idCounter++;
6041
- const payload = {
6042
- jsonrpc: "2.0",
6043
- id,
6044
- method,
6045
- params: params ?? {}
6046
- };
6047
- const payloads = await this.postMessage(payload);
6048
- const result = this.extractResult(payloads, id);
6049
- if (result.error) {
6050
- throw new Error(result.error.message ?? `MCP error on ${method}`);
6047
+ for (let attempt = 0; attempt < 2; attempt++) {
6048
+ try {
6049
+ await this.ensureInitialized();
6050
+ const id = this.idCounter++;
6051
+ const payload = {
6052
+ jsonrpc: "2.0",
6053
+ id,
6054
+ method,
6055
+ params: params ?? {}
6056
+ };
6057
+ const payloads = await this.postMessage(payload);
6058
+ const result = this.extractResult(payloads, id);
6059
+ if (result.error) {
6060
+ throw new Error(result.error.message ?? `MCP error on ${method}`);
6061
+ }
6062
+ return result.result;
6063
+ } catch (error) {
6064
+ if (error instanceof McpSessionExpiredError && attempt === 0) {
6065
+ this.sessionId = void 0;
6066
+ this.initialized = false;
6067
+ continue;
6068
+ }
6069
+ throw error;
6070
+ }
6051
6071
  }
6052
- return result.result;
6072
+ throw new Error(`MCP request to ${method} failed after session retry`);
6053
6073
  }
6054
6074
  async ensureInitialized() {
6055
6075
  if (this.initialized) {
@@ -6105,6 +6125,8 @@ var StreamableHttpMcpRpcClient = class {
6105
6125
  }
6106
6126
  }
6107
6127
  };
6128
+ var TENANT_CLIENT_TTL_MS = 15 * 60 * 1e3;
6129
+ var tenantClientKey = (serverName, tenantId) => `${serverName}\0${tenantId}`;
6108
6130
  var LocalMcpBridge = class {
6109
6131
  remoteServers;
6110
6132
  rpcClients = /* @__PURE__ */ new Map();
@@ -6112,6 +6134,18 @@ var LocalMcpBridge = class {
6112
6134
  unavailableServers = /* @__PURE__ */ new Map();
6113
6135
  authFailedServers = /* @__PURE__ */ new Set();
6114
6136
  envResolver;
6137
+ /**
6138
+ * Per-tenant MCP client cache. For consumer/SaaS deployments where every
6139
+ * call resolves a different bearer token, building a fresh
6140
+ * `StreamableHttpMcpRpcClient` per call would force a fresh `initialize`
6141
+ * round-trip every time. We keep one client per `(serverName, tenantId)`
6142
+ * with TTL-based idle eviction; on token rotation we evict the entry
6143
+ * lazily and rebuild.
6144
+ */
6145
+ tenantClients = /* @__PURE__ */ new Map();
6146
+ /** Test/observability hook: bumped every time a new tenant client is constructed. */
6147
+ tenantClientConstructions = 0;
6148
+ tenantClientTtlMs;
6115
6149
  /**
6116
6150
  * Set a resolver for per-tenant env vars (e.g. MCP auth tokens).
6117
6151
  * Called by the harness after creating the secrets store.
@@ -6119,7 +6153,8 @@ var LocalMcpBridge = class {
6119
6153
  setEnvResolver(resolver) {
6120
6154
  this.envResolver = resolver;
6121
6155
  }
6122
- constructor(config) {
6156
+ constructor(config, options) {
6157
+ this.tenantClientTtlMs = options?.tenantClientTtlMs ?? TENANT_CLIENT_TTL_MS;
6123
6158
  this.remoteServers = (config?.mcp ?? []).filter(
6124
6159
  (entry) => typeof entry.url === "string"
6125
6160
  );
@@ -6290,6 +6325,35 @@ var LocalMcpBridge = class {
6290
6325
  await client.close();
6291
6326
  }
6292
6327
  this.rpcClients.clear();
6328
+ for (const [, entry] of this.tenantClients) {
6329
+ await entry.client.close();
6330
+ }
6331
+ this.tenantClients.clear();
6332
+ }
6333
+ getOrCreateTenantClient(serverName, tenantId, token, server) {
6334
+ const key = tenantClientKey(serverName, tenantId);
6335
+ const now2 = Date.now();
6336
+ const existing = this.tenantClients.get(key);
6337
+ if (existing) {
6338
+ const idle = now2 - existing.lastUsed > this.tenantClientTtlMs;
6339
+ const tokenChanged = existing.token !== token;
6340
+ if (idle || tokenChanged) {
6341
+ void existing.client.close();
6342
+ this.tenantClients.delete(key);
6343
+ } else {
6344
+ existing.lastUsed = now2;
6345
+ return existing.client;
6346
+ }
6347
+ }
6348
+ const client = new StreamableHttpMcpRpcClient(
6349
+ server.url,
6350
+ server.timeoutMs ?? 1e4,
6351
+ token,
6352
+ server.headers
6353
+ );
6354
+ this.tenantClients.set(key, { client, token, lastUsed: now2 });
6355
+ this.tenantClientConstructions += 1;
6356
+ return client;
6293
6357
  }
6294
6358
  listServers() {
6295
6359
  return [...this.remoteServers];
@@ -6411,15 +6475,15 @@ var LocalMcpBridge = class {
6411
6475
  try {
6412
6476
  const tokenEnv = server?.auth?.tokenEnv;
6413
6477
  let callClient = client;
6414
- if (tokenEnv && this.envResolver && context?.tenantId) {
6478
+ if (tokenEnv && this.envResolver && context?.tenantId && server) {
6415
6479
  const tenantToken = await this.envResolver(context.tenantId, tokenEnv);
6416
6480
  const defaultToken = process.env[tokenEnv];
6417
6481
  if (tenantToken && tenantToken !== defaultToken) {
6418
- callClient = new StreamableHttpMcpRpcClient(
6419
- server.url,
6420
- server.timeoutMs ?? 1e4,
6482
+ callClient = this.getOrCreateTenantClient(
6483
+ serverName,
6484
+ context.tenantId,
6421
6485
  tenantToken,
6422
- server.headers
6486
+ server
6423
6487
  );
6424
6488
  }
6425
6489
  }
@@ -8640,6 +8704,7 @@ var AgentHarness = class _AgentHarness {
8640
8704
  reminderStore;
8641
8705
  secretsStore;
8642
8706
  loadedConfig;
8707
+ injectedConfig;
8643
8708
  loadedSkills = [];
8644
8709
  skillFingerprint = "";
8645
8710
  lastSkillRefreshAt = 0;
@@ -8654,6 +8719,8 @@ var AgentHarness = class _AgentHarness {
8654
8719
  _browserMod;
8655
8720
  parsedAgent;
8656
8721
  agentFileFingerprint = "";
8722
+ injectedAgentDefinition;
8723
+ injectedStorageEngine = false;
8657
8724
  mcpBridge;
8658
8725
  subagentManager;
8659
8726
  archivedToolResultsByConversation = /* @__PURE__ */ new Map();
@@ -8821,6 +8888,17 @@ var AgentHarness = class _AgentHarness {
8821
8888
  this.modelProviderInjected = !!options.modelProvider;
8822
8889
  this.modelProvider = options.modelProvider ?? createModelProvider("anthropic");
8823
8890
  this.uploadStore = options.uploadStore;
8891
+ this.injectedConfig = options.config;
8892
+ if (options.agentDefinition !== void 0 && options.storageEngine === void 0) {
8893
+ throw new Error(
8894
+ "HarnessOptions.agentDefinition requires HarnessOptions.storageEngine \u2014 construct a StorageEngine with the desired agentId and pass both."
8895
+ );
8896
+ }
8897
+ this.injectedAgentDefinition = options.agentDefinition;
8898
+ if (options.storageEngine) {
8899
+ this.storageEngine = options.storageEngine;
8900
+ this.injectedStorageEngine = true;
8901
+ }
8824
8902
  if (options.toolDefinitions?.length) {
8825
8903
  this.dispatcher.registerMany(options.toolDefinitions);
8826
8904
  }
@@ -9237,6 +9315,9 @@ var AgentHarness = class _AgentHarness {
9237
9315
  if (this.environment !== "development") {
9238
9316
  return false;
9239
9317
  }
9318
+ if (this.injectedAgentDefinition !== void 0) {
9319
+ return false;
9320
+ }
9240
9321
  try {
9241
9322
  const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9242
9323
  const rawContent = await readFile8(agentFilePath, "utf8");
@@ -9304,15 +9385,23 @@ var AgentHarness = class _AgentHarness {
9304
9385
  }
9305
9386
  }
9306
9387
  async initialize() {
9307
- const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9308
- const agentRawContent = await readFile8(agentFilePath, "utf8");
9309
- this.parsedAgent = parseAgentMarkdown(agentRawContent);
9310
- this.agentFileFingerprint = agentRawContent;
9311
- const identity = await ensureAgentIdentity(this.workingDir);
9312
- if (!this.parsedAgent.frontmatter.id) {
9313
- this.parsedAgent.frontmatter.id = identity.id;
9388
+ if (this.injectedAgentDefinition !== void 0) {
9389
+ this.parsedAgent = typeof this.injectedAgentDefinition === "string" ? parseAgentMarkdown(this.injectedAgentDefinition) : this.injectedAgentDefinition;
9390
+ this.agentFileFingerprint = "";
9391
+ if (this.storageEngine) {
9392
+ this.parsedAgent.frontmatter.id = this.storageEngine.agentId;
9393
+ }
9394
+ } else {
9395
+ const agentFilePath = resolve11(this.workingDir, "AGENT.md");
9396
+ const agentRawContent = await readFile8(agentFilePath, "utf8");
9397
+ this.parsedAgent = parseAgentMarkdown(agentRawContent);
9398
+ this.agentFileFingerprint = agentRawContent;
9399
+ const identity = await ensureAgentIdentity(this.workingDir);
9400
+ if (!this.parsedAgent.frontmatter.id) {
9401
+ this.parsedAgent.frontmatter.id = identity.id;
9402
+ }
9314
9403
  }
9315
- const config = await loadPonchoConfig(this.workingDir);
9404
+ const config = this.injectedConfig ?? await loadPonchoConfig(this.workingDir);
9316
9405
  this.loadedConfig = config;
9317
9406
  this.registerConfiguredBuiltInTools(config);
9318
9407
  const provider = this.parsedAgent.frontmatter.model?.provider ?? "anthropic";
@@ -9328,15 +9417,21 @@ var AgentHarness = class _AgentHarness {
9328
9417
  this.skillFingerprint = this.buildSkillFingerprint(skillMetadata);
9329
9418
  this.registerSkillTools();
9330
9419
  const agentId = this.parsedAgent.frontmatter.id ?? this.parsedAgent.frontmatter.name;
9331
- const storageProvider = config?.storage?.provider ?? "sqlite";
9332
- const engine = createStorageEngine({
9333
- provider: storageProvider,
9334
- workingDir: this.workingDir,
9335
- agentId,
9336
- urlEnv: config?.storage?.urlEnv
9337
- });
9338
- await engine.initialize();
9339
- this.storageEngine = engine;
9420
+ let engine;
9421
+ if (this.injectedStorageEngine && this.storageEngine) {
9422
+ engine = this.storageEngine;
9423
+ await engine.initialize();
9424
+ } else {
9425
+ const storageProvider = config?.storage?.provider ?? "sqlite";
9426
+ engine = createStorageEngine({
9427
+ provider: storageProvider,
9428
+ workingDir: this.workingDir,
9429
+ agentId,
9430
+ urlEnv: config?.storage?.urlEnv
9431
+ });
9432
+ await engine.initialize();
9433
+ this.storageEngine = engine;
9434
+ }
9340
9435
  const maxFileSize = config?.storage?.limits?.maxFileSize ?? 100 * 1024 * 1024;
9341
9436
  const maxTotalStorage = config?.storage?.limits?.maxTotalStorage ?? 1024 * 1024 * 1024;
9342
9437
  const bashWorkingDir = this.environment === "production" ? null : this.workingDir;
@@ -9559,6 +9654,10 @@ var AgentHarness = class _AgentHarness {
9559
9654
  listSkills() {
9560
9655
  return this.loadedSkills.map((s) => ({ name: s.name, description: s.description }));
9561
9656
  }
9657
+ async listSkillsForTenant(tenantId) {
9658
+ const skills = await this.getSkillsForTenant(tenantId);
9659
+ return skills.map((s) => ({ name: s.name, description: s.description }));
9660
+ }
9562
9661
  /**
9563
9662
  * Wraps the run() generator with an OTel root span (invoke_agent) so all
9564
9663
  * child spans (LLM calls via AI SDK, tool execution) group under one trace.
@@ -12896,6 +12995,311 @@ ${resultBody}`,
12896
12995
  }
12897
12996
  };
12898
12997
 
12998
+ // src/orchestrator/run-conversation-turn.ts
12999
+ import { randomUUID as randomUUID6 } from "crypto";
13000
+ import { createLogger as createLogger7 } from "@poncho-ai/sdk";
13001
+ var log = createLogger7("orchestrator");
13002
+ var runConversationTurn = async (opts) => {
13003
+ const conversation = await opts.conversationStore.getWithArchive(opts.conversationId);
13004
+ if (!conversation) {
13005
+ throw new Error(`Conversation not found: ${opts.conversationId}`);
13006
+ }
13007
+ const canonicalHistory = resolveRunRequest(conversation, {
13008
+ conversationId: opts.conversationId,
13009
+ messages: conversation.messages
13010
+ });
13011
+ const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
13012
+ const harnessMessages = [...canonicalHistory.messages];
13013
+ const historyMessages = [...conversation.messages];
13014
+ const preRunMessages = [...conversation.messages];
13015
+ let userContent = opts.task;
13016
+ if (opts.files && opts.files.length > 0 && opts.harness.uploadStore) {
13017
+ const uploadedParts = await Promise.all(
13018
+ opts.files.map(async (f) => {
13019
+ const buf = Buffer.from(f.data, "base64");
13020
+ const key = deriveUploadKey(buf, f.mediaType);
13021
+ const ref = await opts.harness.uploadStore.put(key, buf, f.mediaType);
13022
+ return {
13023
+ type: "file",
13024
+ data: ref,
13025
+ mediaType: f.mediaType,
13026
+ filename: f.filename
13027
+ };
13028
+ })
13029
+ );
13030
+ userContent = [
13031
+ { type: "text", text: opts.task },
13032
+ ...uploadedParts
13033
+ ];
13034
+ }
13035
+ const turnTimestamp = Date.now();
13036
+ const userMessage = {
13037
+ role: "user",
13038
+ content: userContent,
13039
+ metadata: { id: randomUUID6(), timestamp: turnTimestamp }
13040
+ };
13041
+ const assistantId = randomUUID6();
13042
+ const draft = createTurnDraftState();
13043
+ let latestRunId = conversation.runtimeRunId ?? "";
13044
+ let runCancelled = false;
13045
+ let runContinuationMessages;
13046
+ let cancelHarnessMessages;
13047
+ let checkpointedRun = false;
13048
+ const buildMessages = () => {
13049
+ const draftSections = cloneSections(draft.sections);
13050
+ if (draft.currentTools.length > 0) {
13051
+ draftSections.push({ type: "tools", content: [...draft.currentTools] });
13052
+ }
13053
+ if (draft.currentText.length > 0) {
13054
+ draftSections.push({ type: "text", content: draft.currentText });
13055
+ }
13056
+ const userTurn = [userMessage];
13057
+ const hasDraftContent = draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
13058
+ if (!hasDraftContent) {
13059
+ return [...historyMessages, ...userTurn];
13060
+ }
13061
+ return [
13062
+ ...historyMessages,
13063
+ ...userTurn,
13064
+ {
13065
+ role: "assistant",
13066
+ content: draft.assistantResponse,
13067
+ metadata: buildAssistantMetadata(draft, draftSections, {
13068
+ id: assistantId,
13069
+ timestamp: turnTimestamp
13070
+ })
13071
+ }
13072
+ ];
13073
+ };
13074
+ const persistDraft = async () => {
13075
+ if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
13076
+ conversation.messages = buildMessages();
13077
+ conversation.updatedAt = Date.now();
13078
+ await opts.conversationStore.update(conversation);
13079
+ };
13080
+ conversation.messages = [...historyMessages, userMessage];
13081
+ conversation.subagentCallbackCount = 0;
13082
+ conversation._continuationCount = void 0;
13083
+ conversation.updatedAt = Date.now();
13084
+ opts.conversationStore.update(conversation).catch((err) => {
13085
+ log.error(
13086
+ `failed to persist user turn: ${err instanceof Error ? err.message : String(err)}`
13087
+ );
13088
+ });
13089
+ try {
13090
+ const execution = await executeConversationTurn({
13091
+ harness: opts.harness,
13092
+ runInput: {
13093
+ task: opts.task,
13094
+ conversationId: opts.conversationId,
13095
+ tenantId: opts.tenantId ?? void 0,
13096
+ parameters: withToolResultArchiveParam(
13097
+ {
13098
+ ...opts.parameters ?? {},
13099
+ __activeConversationId: opts.conversationId,
13100
+ __ownerId: conversation.ownerId
13101
+ },
13102
+ conversation
13103
+ ),
13104
+ messages: harnessMessages,
13105
+ files: opts.files && opts.files.length > 0 ? opts.files : void 0,
13106
+ abortSignal: opts.abortSignal
13107
+ },
13108
+ initialContextTokens: conversation.contextTokens ?? 0,
13109
+ initialContextWindow: conversation.contextWindow ?? 0,
13110
+ onEvent: async (event, eventDraft) => {
13111
+ draft.assistantResponse = eventDraft.assistantResponse;
13112
+ draft.toolTimeline = eventDraft.toolTimeline;
13113
+ draft.sections = eventDraft.sections;
13114
+ draft.currentTools = eventDraft.currentTools;
13115
+ draft.currentText = eventDraft.currentText;
13116
+ if (event.type === "run:started") {
13117
+ latestRunId = event.runId;
13118
+ }
13119
+ if (event.type === "run:cancelled") {
13120
+ runCancelled = true;
13121
+ if (event.messages) cancelHarnessMessages = event.messages;
13122
+ }
13123
+ if (event.type === "compaction:completed") {
13124
+ if (event.compactedMessages) {
13125
+ historyMessages.length = 0;
13126
+ historyMessages.push(...event.compactedMessages);
13127
+ const preservedFromHistory = historyMessages.length - 1;
13128
+ const removedCount = preRunMessages.length - Math.max(0, preservedFromHistory);
13129
+ const existingHistory = conversation.compactedHistory ?? [];
13130
+ conversation.compactedHistory = [
13131
+ ...existingHistory,
13132
+ ...preRunMessages.slice(0, removedCount)
13133
+ ];
13134
+ }
13135
+ }
13136
+ if (event.type === "step:completed") {
13137
+ await persistDraft();
13138
+ }
13139
+ if (event.type === "tool:approval:required") {
13140
+ const toolText = `- approval required \`${event.tool}\``;
13141
+ draft.toolTimeline.push(toolText);
13142
+ draft.currentTools.push(toolText);
13143
+ const existing = Array.isArray(conversation.pendingApprovals) ? conversation.pendingApprovals : [];
13144
+ if (!existing.some((a) => a.approvalId === event.approvalId)) {
13145
+ conversation.pendingApprovals = [
13146
+ ...existing,
13147
+ {
13148
+ approvalId: event.approvalId,
13149
+ runId: latestRunId || conversation.runtimeRunId || "",
13150
+ tool: event.tool,
13151
+ toolCallId: void 0,
13152
+ input: event.input ?? {},
13153
+ checkpointMessages: void 0,
13154
+ baseMessageCount: historyMessages.length,
13155
+ pendingToolCalls: []
13156
+ }
13157
+ ];
13158
+ conversation.updatedAt = Date.now();
13159
+ await opts.conversationStore.update(conversation);
13160
+ }
13161
+ await persistDraft();
13162
+ }
13163
+ if (event.type === "tool:approval:checkpoint") {
13164
+ conversation.messages = buildMessages();
13165
+ conversation.pendingApprovals = buildApprovalCheckpoints({
13166
+ approvals: event.approvals,
13167
+ runId: latestRunId,
13168
+ checkpointMessages: event.checkpointMessages,
13169
+ baseMessageCount: historyMessages.length,
13170
+ pendingToolCalls: event.pendingToolCalls
13171
+ });
13172
+ conversation._toolResultArchive = opts.harness.getToolResultArchive(
13173
+ opts.conversationId
13174
+ );
13175
+ conversation.updatedAt = Date.now();
13176
+ await opts.conversationStore.update(conversation);
13177
+ checkpointedRun = true;
13178
+ }
13179
+ if (event.type === "run:completed") {
13180
+ if (event.result.continuation && event.result.continuationMessages) {
13181
+ runContinuationMessages = event.result.continuationMessages;
13182
+ conversation.messages = buildMessages();
13183
+ conversation._continuationMessages = runContinuationMessages;
13184
+ conversation._harnessMessages = runContinuationMessages;
13185
+ conversation._toolResultArchive = opts.harness.getToolResultArchive(
13186
+ opts.conversationId
13187
+ );
13188
+ conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
13189
+ if (!checkpointedRun) {
13190
+ conversation.pendingApprovals = [];
13191
+ }
13192
+ if ((event.result.contextTokens ?? 0) > 0) {
13193
+ conversation.contextTokens = event.result.contextTokens;
13194
+ }
13195
+ if ((event.result.contextWindow ?? 0) > 0) {
13196
+ conversation.contextWindow = event.result.contextWindow;
13197
+ }
13198
+ conversation.updatedAt = Date.now();
13199
+ await opts.conversationStore.update(conversation);
13200
+ }
13201
+ }
13202
+ if (opts.onEvent) {
13203
+ await opts.onEvent(event);
13204
+ }
13205
+ }
13206
+ });
13207
+ flushTurnDraft(draft);
13208
+ latestRunId = execution.latestRunId || latestRunId;
13209
+ if (!checkpointedRun && !runContinuationMessages) {
13210
+ conversation.messages = buildMessages();
13211
+ applyTurnMetadata(
13212
+ conversation,
13213
+ {
13214
+ latestRunId,
13215
+ contextTokens: execution.runContextTokens,
13216
+ contextWindow: execution.runContextWindow,
13217
+ harnessMessages: execution.runHarnessMessages,
13218
+ toolResultArchive: opts.harness.getToolResultArchive(opts.conversationId)
13219
+ },
13220
+ { shouldRebuildCanonical }
13221
+ );
13222
+ await opts.conversationStore.update(conversation);
13223
+ }
13224
+ return {
13225
+ latestRunId,
13226
+ cancelled: runCancelled,
13227
+ errored: false,
13228
+ continuation: !!runContinuationMessages,
13229
+ checkpointed: checkpointedRun,
13230
+ contextTokens: execution.runContextTokens,
13231
+ contextWindow: execution.runContextWindow
13232
+ };
13233
+ } catch (error) {
13234
+ flushTurnDraft(draft);
13235
+ const aborted = opts.abortSignal?.aborted === true;
13236
+ if (aborted || runCancelled) {
13237
+ if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
13238
+ conversation.messages = buildMessages();
13239
+ applyTurnMetadata(
13240
+ conversation,
13241
+ {
13242
+ latestRunId,
13243
+ contextTokens: 0,
13244
+ contextWindow: 0,
13245
+ harnessMessages: cancelHarnessMessages,
13246
+ toolResultArchive: opts.harness.getToolResultArchive(opts.conversationId)
13247
+ },
13248
+ { shouldRebuildCanonical: true }
13249
+ );
13250
+ await opts.conversationStore.update(conversation);
13251
+ }
13252
+ if (!checkpointedRun) {
13253
+ const fresh = await opts.conversationStore.get(opts.conversationId);
13254
+ if (fresh && Array.isArray(fresh.pendingApprovals) && fresh.pendingApprovals.length > 0) {
13255
+ fresh.pendingApprovals = [];
13256
+ await opts.conversationStore.update(fresh);
13257
+ }
13258
+ }
13259
+ return {
13260
+ latestRunId,
13261
+ cancelled: true,
13262
+ errored: false,
13263
+ continuation: false,
13264
+ checkpointed: checkpointedRun,
13265
+ contextTokens: 0,
13266
+ contextWindow: 0
13267
+ };
13268
+ }
13269
+ const errorEvent = {
13270
+ type: "run:error",
13271
+ runId: latestRunId || "run_unknown",
13272
+ error: {
13273
+ code: "RUN_ERROR",
13274
+ message: error instanceof Error ? error.message : "Unknown error"
13275
+ }
13276
+ };
13277
+ if (opts.onEvent) {
13278
+ try {
13279
+ await opts.onEvent(errorEvent);
13280
+ } catch (hookErr) {
13281
+ log.error(
13282
+ `onEvent threw on run:error: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`
13283
+ );
13284
+ }
13285
+ }
13286
+ if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
13287
+ conversation.messages = buildMessages();
13288
+ conversation.updatedAt = Date.now();
13289
+ await opts.conversationStore.update(conversation);
13290
+ }
13291
+ return {
13292
+ latestRunId,
13293
+ cancelled: false,
13294
+ errored: true,
13295
+ continuation: false,
13296
+ checkpointed: checkpointedRun,
13297
+ contextTokens: 0,
13298
+ contextWindow: 0
13299
+ };
13300
+ }
13301
+ };
13302
+
12899
13303
  // src/index.ts
12900
13304
  import { defineTool as defineTool13 } from "@poncho-ai/sdk";
12901
13305
  export {
@@ -13003,6 +13407,7 @@ export {
13003
13407
  resolveRunRequest,
13004
13408
  resolveSkillDirs,
13005
13409
  resolveStateConfig,
13410
+ runConversationTurn,
13006
13411
  slugifyStorageComponent,
13007
13412
  startOpenAICodexDeviceAuth,
13008
13413
  verifyTenantToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.40.1",
3
+ "version": "0.42.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",