@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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +48 -0
- package/dist/index.d.ts +92 -1
- package/dist/index.js +191 -61
- package/package.json +2 -2
- package/src/agent-parser.ts +15 -1
- package/src/compaction.ts +163 -15
- package/src/harness.ts +18 -2
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/orchestrator.ts +75 -44
- package/src/orchestrator/run-conversation-turn.ts +7 -0
- package/src/orchestrator/turn.ts +130 -0
- package/src/state.ts +13 -0
- package/test/compaction.test.ts +124 -2
- package/test/resume-transcript-integrity.test.ts +156 -0
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
|
-
|
|
363
|
+
keepRecentTurns: 4,
|
|
364
|
+
keepRecentMessages: 6
|
|
363
365
|
};
|
|
364
|
-
var SUMMARIZATION_MESSAGE_TRUNCATION_CHARS =
|
|
365
|
-
var SUMMARIZATION_MAX_OUTPUT_TOKENS =
|
|
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. **
|
|
372
|
-
5. **
|
|
373
|
-
6. **
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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]
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
13215
|
-
const
|
|
13216
|
-
|
|
13217
|
-
|
|
13218
|
-
|
|
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 =
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
14689
|
+
metadata: { id: randomUUID8(), timestamp: turnTimestamp }
|
|
14565
14690
|
};
|
|
14566
|
-
const assistantId =
|
|
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.
|
|
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.
|
|
37
|
+
"@poncho-ai/sdk": "1.17.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"esbuild": ">=0.17.0",
|
package/src/agent-parser.ts
CHANGED
|
@@ -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
|
};
|