@poncho-ai/harness 0.60.1 → 0.61.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -232,9 +232,10 @@ var parseAgentMarkdown = (content) => {
232
232
  "Invalid AGENT.md frontmatter compaction.trigger: must be between 0.1 and 1."
233
233
  );
234
234
  }
235
+ const keepRecentTurns = typeof raw.keepRecentTurns === "number" ? Math.max(1, Math.floor(raw.keepRecentTurns)) : 4;
235
236
  const keepRecentMessages = typeof raw.keepRecentMessages === "number" ? Math.max(2, Math.floor(raw.keepRecentMessages)) : 6;
236
237
  const instructions = typeof raw.instructions === "string" && raw.instructions.trim() ? raw.instructions.trim() : void 0;
237
- return { enabled, trigger, keepRecentMessages, instructions };
238
+ return { enabled, trigger, keepRecentTurns, keepRecentMessages, instructions };
238
239
  })(),
239
240
  cron: parseCronJobs(parsed.cron)
240
241
  };
@@ -359,29 +360,37 @@ var MIN_COMPACTABLE_MESSAGES = 4;
359
360
  var DEFAULT_COMPACTION_CONFIG = {
360
361
  enabled: true,
361
362
  trigger: 0.75,
362
- keepRecentMessages: 4
363
+ keepRecentTurns: 4,
364
+ keepRecentMessages: 6
363
365
  };
364
- var SUMMARIZATION_MESSAGE_TRUNCATION_CHARS = 1200;
365
- var SUMMARIZATION_MAX_OUTPUT_TOKENS = 768;
366
+ var SUMMARIZATION_MESSAGE_TRUNCATION_CHARS = 4e3;
367
+ var SUMMARIZATION_MAX_OUTPUT_TOKENS = 8192;
368
+ var SUMMARIZATION_MAX_INPUT_TOKENS = 12e4;
366
369
  var SUMMARIZATION_PROMPT = `Summarize the following conversation into a structured working state that allows continuation without re-asking questions. Include:
367
370
 
368
371
  1. **User intent**: What the user originally asked for and any refinements
369
- 2. **Completed work**: What has been accomplished so far
372
+ 2. **Completed work**: What has been accomplished AND CONFIRMED so far
370
373
  3. **Key decisions**: Technical decisions made and their rationale
371
- 4. **Errors & fixes**: Errors encountered and how they were resolved
372
- 5. **Referenced resources**: Files, URLs, tools, or data referenced
373
- 6. **Pending next steps**: What remains to be done
374
+ 4. **Unresolved errors & failures**: Every error, failed or uncertain tool call, and failed or incomplete subagent \u2014 recorded verbatim enough to act on. NEVER drop a failure because it is old. This section is REQUIRED whenever any failure occurred; do not omit it.
375
+ 5. **Pending promises**: Anything the assistant said it WOULD do but has not confirmed done.
376
+ 6. **Referenced resources**: Files, URLs, tools, or data referenced
377
+ 7. **Pending next steps**: What remains to be done
378
+
379
+ Rules:
380
+ - NEVER describe work as "completed", "done", or "fixed" unless the conversation contains explicit confirmation (a successful tool result or the user acknowledging it). If completion is unverified, record it under "Pending next steps" as unconfirmed.
381
+ - Preserve identifiers verbatim \u2014 file paths, URLs, IDs, command names, tool names. Do not paraphrase or truncate them.
374
382
 
375
383
  Be concise but preserve all information needed to continue the task.
376
- Omit any section that has no relevant content.`;
384
+ Omit any section that has no relevant content, EXCEPT "Unresolved errors & failures" when a failure occurred.`;
377
385
  var CUMULATIVE_SUMMARY_PROMPT = `The FIRST message below (tagged [prior-summary]) is an existing working-state summary produced by an earlier compaction. Treat it as the authoritative prior working state: MERGE AND UPDATE it with the newer messages that follow it, carrying forward all still-relevant detail. Do NOT discard or re-compress information from the prior summary just because it is older \u2014 only drop it if the newer messages explicitly supersede it.`;
378
- var SUBAGENT_DIGEST_CHARS = 500;
386
+ var SUBAGENT_DIGEST_CHARS = 2e3;
379
387
  var SUBAGENT_LEDGER_HEADING = "## Subagents";
380
388
  var resolveCompactionConfig = (explicit) => {
381
389
  if (!explicit) return { ...DEFAULT_COMPACTION_CONFIG };
382
390
  return {
383
391
  enabled: explicit.enabled ?? DEFAULT_COMPACTION_CONFIG.enabled,
384
392
  trigger: explicit.trigger ?? DEFAULT_COMPACTION_CONFIG.trigger,
393
+ keepRecentTurns: explicit.keepRecentTurns ?? DEFAULT_COMPACTION_CONFIG.keepRecentTurns,
385
394
  keepRecentMessages: explicit.keepRecentMessages ?? DEFAULT_COMPACTION_CONFIG.keepRecentMessages,
386
395
  instructions: explicit.instructions
387
396
  };
@@ -433,18 +442,57 @@ var findSafeSplitPoint = (messages, keepRecentMessages) => {
433
442
  }
434
443
  return -1;
435
444
  };
445
+ var findSafeSplitPointByTurns = (messages, keepRecentTurns, keepRecentMessagesFallback, maxPreservedTokens) => {
446
+ const userIdx = [];
447
+ for (let i = 0; i < messages.length; i++) {
448
+ if (messages[i].role === "user") userIdx.push(i);
449
+ }
450
+ const maxN = Math.min(keepRecentTurns, userIdx.length);
451
+ for (let n = maxN; n >= 1; n--) {
452
+ let guardN = n;
453
+ let split = userIdx[userIdx.length - guardN];
454
+ while (splitOrphansToolCalls(messages, split) && guardN < userIdx.length) {
455
+ guardN++;
456
+ split = userIdx[userIdx.length - guardN];
457
+ }
458
+ if (split < MIN_COMPACTABLE_MESSAGES) continue;
459
+ const preservedTokens = estimateTokens(
460
+ messages.slice(split).map((m) => getTextContent(m)).join("\n")
461
+ );
462
+ if (preservedTokens <= maxPreservedTokens) return split;
463
+ }
464
+ return findSafeSplitPoint(messages, keepRecentMessagesFallback);
465
+ };
436
466
  var isCompactionSummary = (msg) => msg.metadata?.isCompactionSummary === true;
467
+ var FAILURE_SIGNAL_RE = /\berror\b|\bfailed\b|\bfailure\b|\bexception\b|could ?n[’']?t|cannot\b|unable to|\[subagent result\]/i;
437
468
  var buildSummarizationMessages = (messagesToCompact, instructions) => {
438
469
  const hasPriorSummary = messagesToCompact.length > 0 && isCompactionSummary(messagesToCompact[0]);
439
- const conversationLines = [];
470
+ const lines = [];
440
471
  for (let i = 0; i < messagesToCompact.length; i++) {
441
472
  const msg = messagesToCompact[i];
442
473
  const text = getTextContent(msg);
443
474
  const isPrior = i === 0 && hasPriorSummary;
444
475
  const rendered = isPrior || text.length <= SUMMARIZATION_MESSAGE_TRUNCATION_CHARS ? text : text.slice(0, SUMMARIZATION_MESSAGE_TRUNCATION_CHARS) + "\n...[truncated]";
445
476
  const tag = isPrior ? "prior-summary" : msg.role;
446
- conversationLines.push(`[${tag}]: ${rendered}`);
477
+ const line = `[${tag}]: ${rendered}`;
478
+ lines.push({
479
+ text: line,
480
+ tokens: estimateTokens(line),
481
+ important: isPrior || FAILURE_SIGNAL_RE.test(text)
482
+ });
447
483
  }
484
+ let total = lines.reduce((sum, l) => sum + l.tokens, 0);
485
+ if (total > SUMMARIZATION_MAX_INPUT_TOKENS) {
486
+ for (let i = 0; i < lines.length && total > SUMMARIZATION_MAX_INPUT_TOKENS; ) {
487
+ if (lines[i].important) {
488
+ i++;
489
+ continue;
490
+ }
491
+ total -= lines[i].tokens;
492
+ lines.splice(i, 1);
493
+ }
494
+ }
495
+ const conversationLines = lines.map((l) => l.text);
448
496
  let prompt = SUMMARIZATION_PROMPT;
449
497
  if (hasPriorSummary) prompt = `${prompt}
450
498
 
@@ -475,7 +523,7 @@ var parseSubagentCallback = (msg) => {
475
523
  const subagentId = typeof meta.subagentId === "string" && meta.subagentId ? meta.subagentId : headerMatch?.[2] ?? "";
476
524
  if (!subagentId) return null;
477
525
  const task = typeof meta.task === "string" && meta.task ? meta.task : headerMatch?.[1] ?? "";
478
- const status = headerMatch?.[3] ?? "completed";
526
+ const status = typeof meta.taskOutcome === "string" && meta.taskOutcome || headerMatch?.[3] || "unknown";
479
527
  const bodyStart = text.indexOf("\n\n");
480
528
  const body = bodyStart >= 0 ? text.slice(bodyStart + 2) : text;
481
529
  const digest = body.length > SUBAGENT_DIGEST_CHARS ? body.slice(0, SUBAGENT_DIGEST_CHARS) + "\u2026" : body;
@@ -535,8 +583,18 @@ ${summary}
535
583
  Continue from where the conversation left off without re-asking questions.`,
536
584
  metadata: { isCompactionSummary: true, timestamp: Date.now() }
537
585
  });
586
+ var MAX_PRESERVED_CONTEXT_FRACTION = 0.5;
538
587
  var compactMessages = async (model, messages, config, options) => {
539
- const splitIdx = findSafeSplitPoint(messages, config.keepRecentMessages);
588
+ const contextWindow = options?.contextWindow && options.contextWindow > 0 ? options.contextWindow : 2e5;
589
+ const maxPreservedTokens = Math.floor(
590
+ contextWindow * MAX_PRESERVED_CONTEXT_FRACTION
591
+ );
592
+ const splitIdx = findSafeSplitPointByTurns(
593
+ messages,
594
+ config.keepRecentTurns,
595
+ config.keepRecentMessages,
596
+ maxPreservedTokens
597
+ );
540
598
  if (splitIdx === -1) {
541
599
  return {
542
600
  compacted: false,
@@ -10790,7 +10848,11 @@ var AgentHarness = class _AgentHarness {
10790
10848
  const modelName = agent.frontmatter.model?.name ?? "claude-opus-4-5";
10791
10849
  const modelInstance = this.modelProvider(modelName);
10792
10850
  const config = resolveCompactionConfig(agent.frontmatter.compaction);
10793
- return compactMessages(modelInstance, messages, config, options);
10851
+ const contextWindow = agent.frontmatter.model?.contextWindow ?? getModelContextWindow(modelName);
10852
+ return compactMessages(modelInstance, messages, config, {
10853
+ ...options,
10854
+ contextWindow: options?.contextWindow ?? contextWindow
10855
+ });
10794
10856
  }
10795
10857
  async *run(input) {
10796
10858
  if (!this.parsedAgent) {
@@ -10811,7 +10873,7 @@ var AgentHarness = class _AgentHarness {
10811
10873
  let agent = this.parsedAgent;
10812
10874
  const runId = `run_${randomUUID5()}`;
10813
10875
  const start = now();
10814
- const maxSteps = agent.frontmatter.limits?.maxSteps ?? 20;
10876
+ const maxSteps = input.maxSteps ?? agent.frontmatter.limits?.maxSteps ?? 20;
10815
10877
  const configuredTimeout = this.runTimeoutSecOverride ?? agent.frontmatter.limits?.timeout;
10816
10878
  const timeoutMs = this.environment === "development" && configuredTimeout == null ? 0 : (configuredTimeout ?? 300) * 1e3;
10817
10879
  const platformMaxDurationSec = Number(process.env.PONCHO_MAX_DURATION) || 0;
@@ -11383,7 +11445,8 @@ ${textContent}` };
11383
11445
  const compactResult = await compactMessages(
11384
11446
  modelInstance,
11385
11447
  messages,
11386
- compactionConfig
11448
+ compactionConfig,
11449
+ { contextWindow }
11387
11450
  );
11388
11451
  if (compactResult.compacted) {
11389
11452
  messages.length = 0;
@@ -12709,6 +12772,7 @@ var resolveRunRequest = (conversation, request) => {
12709
12772
  };
12710
12773
 
12711
12774
  // src/orchestrator/turn.ts
12775
+ import { randomUUID as randomUUID6 } from "crypto";
12712
12776
  var createTurnDraftState = () => ({
12713
12777
  assistantResponse: "",
12714
12778
  toolTimeline: [],
@@ -12930,6 +12994,52 @@ var buildApprovalCheckpoints = ({
12930
12994
  pendingToolCalls,
12931
12995
  kind
12932
12996
  }));
12997
+ var assembleCheckpointMessages = (conversation, checkpoint) => {
12998
+ const n = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
12999
+ const base = n.baseMessageCount != null ? conversation.messages.slice(0, n.baseMessageCount) : [];
13000
+ return [...base, ...n.checkpointMessages ?? []];
13001
+ };
13002
+ var buildToolResultMessage = (assistantMsg, toolResults) => {
13003
+ if (assistantMsg?.role !== "assistant") return void 0;
13004
+ let toolCalls = [];
13005
+ try {
13006
+ const parsed = JSON.parse(typeof assistantMsg.content === "string" ? assistantMsg.content : "");
13007
+ toolCalls = parsed.tool_calls ?? [];
13008
+ } catch {
13009
+ return void 0;
13010
+ }
13011
+ if (toolCalls.length === 0) return void 0;
13012
+ const provided = new Map(toolResults.map((r) => [r.callId, r]));
13013
+ return {
13014
+ role: "tool",
13015
+ content: JSON.stringify(
13016
+ toolCalls.map((tc) => {
13017
+ const r = provided.get(tc.id);
13018
+ return {
13019
+ type: "tool_result",
13020
+ tool_use_id: tc.id,
13021
+ tool_name: r?.toolName ?? tc.name,
13022
+ content: r ? r.error ? `Tool error: ${r.error}` : JSON.stringify(r.result ?? null) : "Tool error: Tool execution deferred (pending approval checkpoint)"
13023
+ };
13024
+ })
13025
+ ),
13026
+ metadata: { timestamp: Date.now(), id: randomUUID6() }
13027
+ };
13028
+ };
13029
+ var buildResumeCheckpoints = ({
13030
+ priorMessages,
13031
+ checkpointEvent,
13032
+ runId,
13033
+ kind = "approval"
13034
+ }) => buildApprovalCheckpoints({
13035
+ approvals: checkpointEvent.approvals,
13036
+ runId,
13037
+ checkpointMessages: [...priorMessages, ...checkpointEvent.checkpointMessages],
13038
+ baseMessageCount: 0,
13039
+ pendingToolCalls: checkpointEvent.pendingToolCalls,
13040
+ kind
13041
+ });
13042
+ var messageText = (m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => p.text ?? "").join("") : "";
12933
13043
  var applyTurnMetadata = (conv, meta, opts = {}) => {
12934
13044
  const {
12935
13045
  clearContinuation = true,
@@ -12948,6 +13058,20 @@ var applyTurnMetadata = (conv, meta, opts = {}) => {
12948
13058
  } else if (shouldRebuildCanonical) {
12949
13059
  conv._harnessMessages = conv.messages;
12950
13060
  }
13061
+ if (isMessageArray(conv._harnessMessages) && conv._harnessMessages.length > 0) {
13062
+ const canonical = conv._harnessMessages;
13063
+ const summarized = canonical.some((m) => m.metadata?.isCompactionSummary);
13064
+ const lastUser = [...conv.messages].reverse().find((m) => m.role === "user");
13065
+ const lastUserText = lastUser ? messageText(lastUser).trim() : "";
13066
+ if (!summarized && lastUserText) {
13067
+ const present = canonical.some((m) => m.role === "user" && messageText(m).trim() === lastUserText);
13068
+ if (!present) {
13069
+ console.error(
13070
+ `[transcript-guard] conversation ${conv.conversationId}: model-facing transcript is missing the latest user message \u2014 it diverged from the display transcript. This is a resume/finalize message-assembly bug; the model will not see that turn's input.`
13071
+ );
13072
+ }
13073
+ }
13074
+ }
12951
13075
  if (meta.toolResultArchive !== void 0) {
12952
13076
  conv._toolResultArchive = meta.toolResultArchive;
12953
13077
  }
@@ -12978,12 +13102,12 @@ var STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
12978
13102
  import { createLogger as createLogger8, getTextContent as getTextContent3 } from "@poncho-ai/sdk";
12979
13103
 
12980
13104
  // src/orchestrator/entries-dual-write.ts
12981
- import { randomUUID as randomUUID6 } from "crypto";
13105
+ import { randomUUID as randomUUID7 } from "crypto";
12982
13106
  var appendEntriesSafe = async (store, conversation, entries, log2) => {
12983
13107
  if (entries.length === 0) return [];
12984
13108
  try {
12985
13109
  const withIds = entries.map(
12986
- (e) => ({ id: randomUUID6(), ...e })
13110
+ (e) => ({ id: randomUUID7(), ...e })
12987
13111
  );
12988
13112
  return await store.appendEntries(
12989
13113
  conversation.conversationId,
@@ -13037,6 +13161,17 @@ var abnormalEndResponse = (opts) => {
13037
13161
 
13038
13162
  ${opts.gathered}` : `${head} ${recover}`;
13039
13163
  };
13164
+ var SUBAGENT_OUTCOME_RE = /\[\[\s*OUTCOME\s*:\s*(succeeded|failed|partial)\s*\]\]/i;
13165
+ var SUBAGENT_OUTCOME_INSTRUCTION = "\n\nAt the very end of your final message, on its own line, append a machine-readable outcome verdict for the task you were given: `[[OUTCOME: succeeded]]`, `[[OUTCOME: partial]]`, or `[[OUTCOME: failed]]`, followed by a one-line reason. Judge honestly by whether YOU actually accomplished the task (produced the real result / wrote the files), not by whether the run finished. If a tool or capability you needed was missing, that is `failed`.";
13166
+ var deriveTaskOutcome = (gathered, abnormal) => {
13167
+ if (abnormal) return "failed";
13168
+ if (!gathered.trim()) return "failed";
13169
+ const m = gathered.match(SUBAGENT_OUTCOME_RE);
13170
+ if (!m) return "unknown";
13171
+ const v = m[1].toLowerCase();
13172
+ return v === "succeeded" ? "succeeded" : v === "partial" ? "partial" : "failed";
13173
+ };
13174
+ var stripOutcomeVerdict = (text) => text.replace(/\n*\[\[\s*OUTCOME\s*:\s*(?:succeeded|failed|partial)\s*\]\].*$/is, "").trimEnd();
13040
13175
  var AgentOrchestrator = class {
13041
13176
  harness;
13042
13177
  conversationStore;
@@ -13211,34 +13346,11 @@ var AgentOrchestrator = class {
13211
13346
  });
13212
13347
  let latestRunId = conversation.runtimeRunId ?? "";
13213
13348
  let checkpointedRun = false;
13214
- const normalizedCheckpoint = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
13215
- const baseMessages = normalizedCheckpoint.baseMessageCount != null ? conversation.messages.slice(0, normalizedCheckpoint.baseMessageCount) : [];
13216
- const fullCheckpointMessages = [...baseMessages, ...normalizedCheckpoint.checkpointMessages];
13217
- let resumeToolResultMsg;
13218
- const lastCpMsg = fullCheckpointMessages[fullCheckpointMessages.length - 1];
13219
- if (lastCpMsg?.role === "assistant") {
13220
- try {
13221
- const parsed = JSON.parse(typeof lastCpMsg.content === "string" ? lastCpMsg.content : "");
13222
- const cpToolCalls = parsed.tool_calls ?? [];
13223
- if (cpToolCalls.length > 0) {
13224
- const providedMap = new Map(toolResults.map((r) => [r.callId, r]));
13225
- resumeToolResultMsg = {
13226
- role: "tool",
13227
- content: JSON.stringify(cpToolCalls.map((tc) => {
13228
- const provided = providedMap.get(tc.id);
13229
- return {
13230
- type: "tool_result",
13231
- tool_use_id: tc.id,
13232
- tool_name: provided?.toolName ?? tc.name,
13233
- content: provided ? provided.error ? `Tool error: ${provided.error}` : JSON.stringify(provided.result ?? null) : "Tool error: Tool execution deferred (pending approval checkpoint)"
13234
- };
13235
- })),
13236
- metadata: { timestamp: Date.now() }
13237
- };
13238
- }
13239
- } catch {
13240
- }
13241
- }
13349
+ const fullCheckpointMessages = assembleCheckpointMessages(conversation, checkpoint);
13350
+ const resumeToolResultMsg = buildToolResultMessage(
13351
+ fullCheckpointMessages[fullCheckpointMessages.length - 1],
13352
+ toolResults
13353
+ );
13242
13354
  const fullCheckpointWithResults = resumeToolResultMsg ? [...fullCheckpointMessages, resumeToolResultMsg] : fullCheckpointMessages;
13243
13355
  let draftRef;
13244
13356
  let execution;
@@ -13283,12 +13395,10 @@ var AgentOrchestrator = class {
13283
13395
  const cpEvent = event;
13284
13396
  const conv = await this.conversationStore.get(conversationId);
13285
13397
  if (conv) {
13286
- conv.pendingApprovals = buildApprovalCheckpoints({
13287
- approvals: cpEvent.approvals,
13288
- runId: latestRunId,
13289
- checkpointMessages: [...fullCheckpointWithResults, ...cpEvent.checkpointMessages],
13290
- baseMessageCount: 0,
13291
- pendingToolCalls: cpEvent.pendingToolCalls
13398
+ conv.pendingApprovals = buildResumeCheckpoints({
13399
+ priorMessages: fullCheckpointWithResults,
13400
+ checkpointEvent: cpEvent,
13401
+ runId: latestRunId
13292
13402
  });
13293
13403
  conv.updatedAt = Date.now();
13294
13404
  await this.conversationStore.update(conv);
@@ -13480,6 +13590,7 @@ var AgentOrchestrator = class {
13480
13590
  subagentId,
13481
13591
  task: conv.subagentMeta?.task ?? conv.title,
13482
13592
  status: "completed",
13593
+ taskOutcome: deriveTaskOutcome(responseText, false),
13483
13594
  result: { status: "completed", response: responseText, steps: 0, tokens: { input: 0, output: 0, cached: 0 }, duration: 0 },
13484
13595
  timestamp: Date.now()
13485
13596
  };
@@ -13581,7 +13692,11 @@ var AgentOrchestrator = class {
13581
13692
  const harnessMessages = [...runOutcome.messages];
13582
13693
  const recallParams = this.hooks?.buildRecallParams?.({ ownerId, tenantId: conversation.tenantId, excludeConversationId: childConversationId }) ?? {};
13583
13694
  for await (const event of childHarness.runWithTelemetry({
13584
- task,
13695
+ // Ask the subagent to self-report a machine-readable task outcome at the
13696
+ // end of its final message; `deriveTaskOutcome` parses it so a failed
13697
+ // task can never be recorded as "completed". Appended only to the model
13698
+ // input, not the stored/displayed task name.
13699
+ task: `${task}${SUBAGENT_OUTCOME_INSTRUCTION}`,
13585
13700
  conversationId: childConversationId,
13586
13701
  tenantId: conversation.tenantId ?? void 0,
13587
13702
  parameters: withToolResultArchiveParam({
@@ -13768,11 +13883,14 @@ var AgentOrchestrator = class {
13768
13883
  if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
13769
13884
  }
13770
13885
  const abnormal = !runResult;
13771
- const subagentResponse = abnormal ? abnormalEndResponse({ subagentId: childConversationId, gathered, runError }) : gathered;
13886
+ const taskOutcome = deriveTaskOutcome(gathered, abnormal);
13887
+ const cleanedGathered = stripOutcomeVerdict(gathered);
13888
+ const subagentResponse = abnormal ? abnormalEndResponse({ subagentId: childConversationId, gathered: cleanedGathered, runError }) : cleanedGathered;
13772
13889
  const pendingResult = {
13773
13890
  subagentId: childConversationId,
13774
13891
  task,
13775
13892
  status: abnormal ? "error" : "completed",
13893
+ taskOutcome,
13776
13894
  result: {
13777
13895
  status: runResult?.status ?? "error",
13778
13896
  response: subagentResponse,
@@ -13813,6 +13931,7 @@ var AgentOrchestrator = class {
13813
13931
  subagentId: childConversationId,
13814
13932
  task,
13815
13933
  status: "error",
13934
+ taskOutcome: "failed",
13816
13935
  error: { code: "SUBAGENT_ERROR", message: errMsg },
13817
13936
  timestamp: Date.now()
13818
13937
  };
@@ -13880,10 +13999,10 @@ Response: ${responseLine}
13880
13999
  Steps: ${pr.result.steps}, Duration: ${pr.result.duration}ms` : pr.error ? `Error: ${pr.error.message}` : "(no result)";
13881
14000
  const injected = {
13882
14001
  role: "user",
13883
- content: `[Subagent Result] Subagent "${pr.task}" (${pr.subagentId}) ${pr.status}:
14002
+ content: `[Subagent Result] Subagent "${pr.task}" (${pr.subagentId}) ${pr.taskOutcome}:
13884
14003
 
13885
14004
  ${resultBody}`,
13886
- metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, timestamp: pr.timestamp }
14005
+ metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, taskOutcome: pr.taskOutcome, timestamp: pr.timestamp }
13887
14006
  };
13888
14007
  injectedCallbackMessages.push(injected);
13889
14008
  conversation.messages.push(injected);
@@ -14166,13 +14285,16 @@ ${resultBody}`,
14166
14285
  if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
14167
14286
  }
14168
14287
  const abnormal = !runResult;
14169
- const subagentResponse = abnormal ? abnormalEndResponse({ subagentId: conversationId, gathered, runError }) : gathered;
14288
+ const taskOutcome = deriveTaskOutcome(gathered, abnormal);
14289
+ const cleanedGathered = stripOutcomeVerdict(gathered);
14290
+ const subagentResponse = abnormal ? abnormalEndResponse({ subagentId: conversationId, gathered: cleanedGathered, runError }) : cleanedGathered;
14170
14291
  const parentConv = await this.conversationStore.get(parentConversationId);
14171
14292
  if (parentConv) {
14172
14293
  const result = {
14173
14294
  subagentId: conversationId,
14174
14295
  task,
14175
14296
  status: abnormal ? "error" : "completed",
14297
+ taskOutcome,
14176
14298
  result: { status: runResult?.status ?? "error", response: subagentResponse, steps: runResult?.steps ?? 0, tokens: { input: 0, output: 0, cached: 0 }, duration: runResult?.duration ?? 0 },
14177
14299
  ...abnormal ? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } } : {},
14178
14300
  timestamp: Date.now()
@@ -14221,6 +14343,7 @@ ${resultBody}`,
14221
14343
  subagentId: conversationId,
14222
14344
  task,
14223
14345
  status: "error",
14346
+ taskOutcome: "failed",
14224
14347
  error: { code: "CONTINUATION_ERROR", message: err instanceof Error ? err.message : String(err) },
14225
14348
  timestamp: Date.now()
14226
14349
  };
@@ -14464,6 +14587,7 @@ ${resultBody}`,
14464
14587
  subagentId: childConversationId,
14465
14588
  task,
14466
14589
  status: "error",
14590
+ taskOutcome: "failed",
14467
14591
  error: { code: "SUBAGENT_SPAWN_FAILED", message },
14468
14592
  timestamp: Date.now()
14469
14593
  });
@@ -14495,6 +14619,7 @@ ${resultBody}`,
14495
14619
  subagentId: conv.conversationId,
14496
14620
  task: conv.subagentMeta.task,
14497
14621
  status: "error",
14622
+ taskOutcome: "failed",
14498
14623
  error: conv.subagentMeta.error,
14499
14624
  timestamp: Date.now()
14500
14625
  };
@@ -14521,7 +14646,7 @@ ${resultBody}`,
14521
14646
  };
14522
14647
 
14523
14648
  // src/orchestrator/run-conversation-turn.ts
14524
- import { randomUUID as randomUUID7 } from "crypto";
14649
+ import { randomUUID as randomUUID8 } from "crypto";
14525
14650
  import { createLogger as createLogger9 } from "@poncho-ai/sdk";
14526
14651
  var log = createLogger9("orchestrator");
14527
14652
  var runConversationTurn = async (opts) => {
@@ -14561,9 +14686,9 @@ var runConversationTurn = async (opts) => {
14561
14686
  const userMessage = {
14562
14687
  role: "user",
14563
14688
  content: userContent,
14564
- metadata: { id: randomUUID7(), timestamp: turnTimestamp }
14689
+ metadata: { id: randomUUID8(), timestamp: turnTimestamp }
14565
14690
  };
14566
- const assistantId = randomUUID7();
14691
+ const assistantId = randomUUID8();
14567
14692
  const draft = createTurnDraftState();
14568
14693
  let latestRunId = conversation.runtimeRunId ?? "";
14569
14694
  let runCancelled = false;
@@ -14637,6 +14762,7 @@ var runConversationTurn = async (opts) => {
14637
14762
  disablePromptCache: opts.disablePromptCache,
14638
14763
  suppressTelemetry: opts.suppressTelemetry,
14639
14764
  model: opts.model,
14765
+ maxSteps: opts.maxSteps,
14640
14766
  volatileContext: opts.volatileContext,
14641
14767
  telemetryAttributes: opts.telemetryAttributes
14642
14768
  },
@@ -14938,11 +15064,14 @@ export {
14938
15064
  abnormalEndResponse,
14939
15065
  appendEntriesSafe,
14940
15066
  applyTurnMetadata,
15067
+ assembleCheckpointMessages,
14941
15068
  buildAgentDirectoryName,
14942
15069
  buildApprovalCheckpoints,
14943
15070
  buildAssistantMetadata,
15071
+ buildResumeCheckpoints,
14944
15072
  buildSkillContextWindow,
14945
15073
  buildToolCompletedText,
15074
+ buildToolResultMessage,
14946
15075
  cloneSections,
14947
15076
  compactMessages,
14948
15077
  completeOpenAICodexDeviceAuth,
@@ -14981,6 +15110,7 @@ export {
14981
15110
  estimateTotalTokens,
14982
15111
  executeConversationTurn,
14983
15112
  findSafeSplitPoint,
15113
+ findSafeSplitPointByTurns,
14984
15114
  flushTurnDraft,
14985
15115
  generateAgentId,
14986
15116
  getAgentStoreDirectory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.60.1",
3
+ "version": "0.61.1",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "mustache": "^4.2.0",
35
35
  "yaml": "^2.4.0",
36
36
  "zod": "^3.22.0",
37
- "@poncho-ai/sdk": "1.16.0"
37
+ "@poncho-ai/sdk": "1.17.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "esbuild": ">=0.17.0",
@@ -52,6 +52,16 @@ export interface AgentFrontmatter {
52
52
  export interface CompactionConfig {
53
53
  enabled: boolean;
54
54
  trigger: number;
55
+ /**
56
+ * Primary retention unit: number of whole recent *turns* (a user message plus
57
+ * the assistant/tool messages that follow it) preserved verbatim after the
58
+ * summary. See `findSafeSplitPointByTurns`.
59
+ */
60
+ keepRecentTurns: number;
61
+ /**
62
+ * Fallback retention floor, in messages, used only for the single-giant-turn
63
+ * case where no turn boundary yields a safe split.
64
+ */
55
65
  keepRecentMessages: number;
56
66
  instructions?: string;
57
67
  }
@@ -306,6 +316,10 @@ export const parseAgentMarkdown = (content: string): ParsedAgent => {
306
316
  "Invalid AGENT.md frontmatter compaction.trigger: must be between 0.1 and 1.",
307
317
  );
308
318
  }
319
+ const keepRecentTurns =
320
+ typeof raw.keepRecentTurns === "number"
321
+ ? Math.max(1, Math.floor(raw.keepRecentTurns))
322
+ : 4;
309
323
  const keepRecentMessages =
310
324
  typeof raw.keepRecentMessages === "number"
311
325
  ? Math.max(2, Math.floor(raw.keepRecentMessages))
@@ -314,7 +328,7 @@ export const parseAgentMarkdown = (content: string): ParsedAgent => {
314
328
  typeof raw.instructions === "string" && raw.instructions.trim()
315
329
  ? raw.instructions.trim()
316
330
  : undefined;
317
- return { enabled, trigger, keepRecentMessages, instructions };
331
+ return { enabled, trigger, keepRecentTurns, keepRecentMessages, instructions };
318
332
  })(),
319
333
  cron: parseCronJobs(parsed.cron),
320
334
  };