@poncho-ai/harness 0.60.0 → 0.61.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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +40 -0
- package/dist/index.d.ts +65 -1
- package/dist/index.js +116 -23
- package/package.json +2 -2
- package/src/agent-parser.ts +15 -1
- package/src/compaction.ts +163 -15
- package/src/harness.ts +27 -2
- package/src/orchestrator/orchestrator.ts +64 -7
- package/src/orchestrator/run-conversation-turn.ts +7 -0
- package/src/state.ts +13 -0
- package/test/compaction.test.ts +124 -2
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.61.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
|
|
3
3
|
> node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[embed-docs] Generated poncho-docs.ts with 4 topics
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
11
|
[32mESM[39m [1mdist/isolate-F2PPSUL6.js [22m[32m53.82 KB[39m
|
|
12
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m579.17 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 267ms
|
|
14
14
|
[34mDTS[39m Build start
|
|
15
|
-
[32mDTS[39m ⚡️ Build success in
|
|
16
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
15
|
+
[32mDTS[39m ⚡️ Build success in 8217ms
|
|
16
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m108.62 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.61.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#192](https://github.com/cesr/poncho-ai/pull/192) [`bfd8012`](https://github.com/cesr/poncho-ai/commit/bfd80125ad71cd4401221c598faebf71afda2078) Thanks [@cesr](https://github.com/cesr)! - Fix context-compaction correctness so the agent stops losing context and
|
|
8
|
+
mislabeling failed work after a compaction:
|
|
9
|
+
- **Turn-based retention.** Compaction now preserves the last N whole _turns_
|
|
10
|
+
verbatim (new `compaction.keepRecentTurns`, default 4) instead of N messages,
|
|
11
|
+
which in tool-heavy turns collapsed to just the summary. The preserved side is
|
|
12
|
+
bounded by a token budget (≤ 50% of the context window) so keeping recent turns
|
|
13
|
+
can't leave the post-compaction context above the trigger (re-compaction
|
|
14
|
+
thrash / overflow). Adds exported `findSafeSplitPointByTurns`.
|
|
15
|
+
- **Faithful summaries.** The summarization output cap is raised 768 → 8192
|
|
16
|
+
tokens (768 physically truncated summaries mid-content); per-message truncation
|
|
17
|
+
1200 → 4000 chars, with a total summarizer-input budget that drops the oldest
|
|
18
|
+
non-error messages first. The prompt now requires a non-omittable "Unresolved
|
|
19
|
+
errors & failures" section, a "Pending promises" section, forbids claiming
|
|
20
|
+
unconfirmed completion, and preserves identifiers verbatim.
|
|
21
|
+
- **Structured subagent task outcome.** New `PendingSubagentResult.taskOutcome`
|
|
22
|
+
(`succeeded | failed | partial | unknown`) distinct from run status: a subagent
|
|
23
|
+
that ran but failed its task is no longer recorded as "completed". Subagents
|
|
24
|
+
self-report a machine-readable verdict; it is parsed deterministically
|
|
25
|
+
(defaulting to "unknown", never success) and rendered in the callback header
|
|
26
|
+
and compaction ledger. The subagent digest is enlarged (2000 chars, ungated on
|
|
27
|
+
status) so the failure reason survives compaction.
|
|
28
|
+
- **Per-run `maxSteps` override.** New `RunInput.maxSteps` lets a caller raise the
|
|
29
|
+
step ceiling for foreground turns without raising it for background/job turns
|
|
30
|
+
that share the same agent definition.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- Updated dependencies [[`bfd8012`](https://github.com/cesr/poncho-ai/commit/bfd80125ad71cd4401221c598faebf71afda2078)]:
|
|
35
|
+
- @poncho-ai/sdk@1.17.0
|
|
36
|
+
|
|
37
|
+
## 0.60.1
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- [`47d92ba`](https://github.com/cesr/poncho-ai/commit/47d92ba6e0001147423ffda39f9dbeb29b5ca359) Thanks [@cesr](https://github.com/cesr)! - Forward `volatileContext` and `telemetryAttributes` through `continueFromToolResult`. The per-conversation capture already covers same-process continuations, but a checkpoint resumed after a process restart would silently lose the embedder's volatile blocks; resume callers can now pass them explicitly.
|
|
42
|
+
|
|
3
43
|
## 0.60.0
|
|
4
44
|
|
|
5
45
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,16 @@ interface AgentFrontmatter {
|
|
|
43
43
|
interface CompactionConfig {
|
|
44
44
|
enabled: boolean;
|
|
45
45
|
trigger: number;
|
|
46
|
+
/**
|
|
47
|
+
* Primary retention unit: number of whole recent *turns* (a user message plus
|
|
48
|
+
* the assistant/tool messages that follow it) preserved verbatim after the
|
|
49
|
+
* summary. See `findSafeSplitPointByTurns`.
|
|
50
|
+
*/
|
|
51
|
+
keepRecentTurns: number;
|
|
52
|
+
/**
|
|
53
|
+
* Fallback retention floor, in messages, used only for the single-giant-turn
|
|
54
|
+
* case where no turn boundary yields a safe split.
|
|
55
|
+
*/
|
|
46
56
|
keepRecentMessages: number;
|
|
47
57
|
instructions?: string;
|
|
48
58
|
}
|
|
@@ -105,8 +115,37 @@ declare const estimateTotalTokens: (systemPrompt: string, messages: Message[], t
|
|
|
105
115
|
* Returns -1 if no valid split point is found.
|
|
106
116
|
*/
|
|
107
117
|
declare const findSafeSplitPoint: (messages: Message[], keepRecentMessages: number) => number;
|
|
118
|
+
/**
|
|
119
|
+
* Find the split index that preserves the last `keepRecentTurns` whole *turns*
|
|
120
|
+
* verbatim. A turn begins at a `role:"user"` message and runs until the next
|
|
121
|
+
* `user` message. Everything before the split is folded into the summary;
|
|
122
|
+
* everything from the split onward is kept as-is.
|
|
123
|
+
*
|
|
124
|
+
* Two guards make a candidate split acceptable:
|
|
125
|
+
* 1. The compacted side has at least `MIN_COMPACTABLE_MESSAGES` messages.
|
|
126
|
+
* 2. The preserved side estimates at most `maxPreservedTokens` — so keeping N
|
|
127
|
+
* large turns can never leave the post-compaction context above the
|
|
128
|
+
* compaction trigger (which caused re-compaction thrash / window overflow).
|
|
129
|
+
* The tool-call-orphan guard (`splitOrphansToolCalls`) is preserved: a split
|
|
130
|
+
* that would strand an assistant tool-call on the compacted side steps to an
|
|
131
|
+
* earlier user boundary (which only preserves more, always safe).
|
|
132
|
+
*
|
|
133
|
+
* We try the largest N (up to `keepRecentTurns`) first and decrement, so we
|
|
134
|
+
* keep as many turns as fit. If no turn boundary yields a safe split (e.g. a
|
|
135
|
+
* single giant turn near the window), fall back to the message-based
|
|
136
|
+
* `findSafeSplitPoint` so the middle of that turn still compacts.
|
|
137
|
+
*
|
|
138
|
+
* Returns -1 when nothing safe can be compacted.
|
|
139
|
+
*/
|
|
140
|
+
declare const findSafeSplitPointByTurns: (messages: Message[], keepRecentTurns: number, keepRecentMessagesFallback: number, maxPreservedTokens: number) => number;
|
|
108
141
|
interface CompactMessagesOptions {
|
|
109
142
|
instructions?: string;
|
|
143
|
+
/**
|
|
144
|
+
* The model's context window (tokens). Used to bound the preserved side of a
|
|
145
|
+
* turn-based split so keeping N recent turns can't leave the post-compaction
|
|
146
|
+
* context above the compaction trigger. Defaults to 200k when omitted.
|
|
147
|
+
*/
|
|
148
|
+
contextWindow?: number;
|
|
110
149
|
}
|
|
111
150
|
interface CompactResult {
|
|
112
151
|
compacted: boolean;
|
|
@@ -207,10 +246,22 @@ interface StateStore {
|
|
|
207
246
|
set(state: ConversationState): Promise<void>;
|
|
208
247
|
delete(runId: string): Promise<void>;
|
|
209
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Whether a subagent actually accomplished its task — distinct from the *run*
|
|
251
|
+
* status (`status` below), which is "completed" whenever the run merely
|
|
252
|
+
* finished. A subagent that ran to completion but could not do the task (e.g.
|
|
253
|
+
* lacked the required tools) is `runStatus:"completed"` but `taskOutcome:
|
|
254
|
+
* "failed"`. Derived at subagent finalize; "unknown" when it can't be
|
|
255
|
+
* determined. Consumed by the compaction subagent ledger so a failed task can
|
|
256
|
+
* never be summarized as "completed".
|
|
257
|
+
*/
|
|
258
|
+
type SubagentTaskOutcome = "succeeded" | "failed" | "partial" | "unknown";
|
|
210
259
|
interface PendingSubagentResult {
|
|
211
260
|
subagentId: string;
|
|
212
261
|
task: string;
|
|
213
262
|
status: "completed" | "error" | "stopped";
|
|
263
|
+
/** Did the subagent accomplish its task? See {@link SubagentTaskOutcome}. */
|
|
264
|
+
taskOutcome: SubagentTaskOutcome;
|
|
214
265
|
result?: _poncho_ai_sdk.RunResult;
|
|
215
266
|
error?: _poncho_ai_sdk.AgentFailure;
|
|
216
267
|
timestamp: number;
|
|
@@ -1635,6 +1686,13 @@ declare class AgentHarness {
|
|
|
1635
1686
|
* otherwise the continuation falls back to the agent definition's
|
|
1636
1687
|
* (possibly concurrently-mutated) frontmatter model. */
|
|
1637
1688
|
model?: string;
|
|
1689
|
+
/** Same as `RunInput.volatileContext`. Pass it explicitly on resume —
|
|
1690
|
+
* the per-conversation capture covers same-process continuations, but
|
|
1691
|
+
* a checkpoint resumed after a restart would otherwise lose the
|
|
1692
|
+
* embedder's volatile blocks. */
|
|
1693
|
+
volatileContext?: string;
|
|
1694
|
+
/** Same as `RunInput.telemetryAttributes`, for the continuation run. */
|
|
1695
|
+
telemetryAttributes?: Record<string, string>;
|
|
1638
1696
|
}): AsyncGenerator<AgentEvent>;
|
|
1639
1697
|
runToCompletion(input: RunInput): Promise<HarnessRunOutput>;
|
|
1640
1698
|
}
|
|
@@ -2397,6 +2455,12 @@ interface RunConversationTurnOpts {
|
|
|
2397
2455
|
* built with an OTLP exporter attached.
|
|
2398
2456
|
*/
|
|
2399
2457
|
suppressTelemetry?: boolean;
|
|
2458
|
+
/**
|
|
2459
|
+
* Forwarded to `RunInput.maxSteps`: per-run step-ceiling override. Lets the
|
|
2460
|
+
* foreground chat turn run a higher ceiling than background/job turns that
|
|
2461
|
+
* share the same agent definition.
|
|
2462
|
+
*/
|
|
2463
|
+
maxSteps?: number;
|
|
2400
2464
|
/**
|
|
2401
2465
|
* Forwarded to `RunInput.telemetryAttributes` — extra attributes for the
|
|
2402
2466
|
* `invoke_agent` root span (e.g. run kind / job name for segmentation).
|
|
@@ -2439,4 +2503,4 @@ type NewEntryNoId = NewConversationEntry extends infer T ? T extends NewConversa
|
|
|
2439
2503
|
*/
|
|
2440
2504
|
declare const appendEntriesSafe: (store: ConversationStore, conversation: Pick<Conversation, "conversationId" | "ownerId" | "tenantId">, entries: NewEntryNoId[], log: Logger) => Promise<ConversationEntry[]>;
|
|
2441
2505
|
|
|
2442
|
-
export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CallbackStartedEntry, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationEntry, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TAGLINE, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, type NewConversationEntry, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentResultEntry, type SubagentSpawnResult, type SubagentSummary, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, abnormalEndResponse, appendEntriesSafe, applyTurnMetadata, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildSkillContextWindow, buildToolCompletedText, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPendingSubagentResults, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, lastAssistantText, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, realResponseText, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, stripPillMetaTokens, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
|
2506
|
+
export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CallbackStartedEntry, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationEntry, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TAGLINE, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, type NewConversationEntry, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentResultEntry, type SubagentSpawnResult, type SubagentSummary, type SubagentTaskOutcome, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, abnormalEndResponse, appendEntriesSafe, applyTurnMetadata, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildSkillContextWindow, buildToolCompletedText, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, findSafeSplitPointByTurns, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPendingSubagentResults, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, lastAssistantText, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, realResponseText, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, stripPillMetaTokens, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
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;
|
|
@@ -12229,7 +12292,9 @@ ${this.skillFingerprint}`;
|
|
|
12229
12292
|
parameters: input.parameters,
|
|
12230
12293
|
abortSignal: input.abortSignal,
|
|
12231
12294
|
suppressTelemetry: input.suppressTelemetry,
|
|
12232
|
-
model: input.model
|
|
12295
|
+
model: input.model,
|
|
12296
|
+
volatileContext: input.volatileContext,
|
|
12297
|
+
telemetryAttributes: input.telemetryAttributes
|
|
12233
12298
|
});
|
|
12234
12299
|
}
|
|
12235
12300
|
async runToCompletion(input) {
|
|
@@ -13035,6 +13100,17 @@ var abnormalEndResponse = (opts) => {
|
|
|
13035
13100
|
|
|
13036
13101
|
${opts.gathered}` : `${head} ${recover}`;
|
|
13037
13102
|
};
|
|
13103
|
+
var SUBAGENT_OUTCOME_RE = /\[\[\s*OUTCOME\s*:\s*(succeeded|failed|partial)\s*\]\]/i;
|
|
13104
|
+
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`.";
|
|
13105
|
+
var deriveTaskOutcome = (gathered, abnormal) => {
|
|
13106
|
+
if (abnormal) return "failed";
|
|
13107
|
+
if (!gathered.trim()) return "failed";
|
|
13108
|
+
const m = gathered.match(SUBAGENT_OUTCOME_RE);
|
|
13109
|
+
if (!m) return "unknown";
|
|
13110
|
+
const v = m[1].toLowerCase();
|
|
13111
|
+
return v === "succeeded" ? "succeeded" : v === "partial" ? "partial" : "failed";
|
|
13112
|
+
};
|
|
13113
|
+
var stripOutcomeVerdict = (text) => text.replace(/\n*\[\[\s*OUTCOME\s*:\s*(?:succeeded|failed|partial)\s*\]\].*$/is, "").trimEnd();
|
|
13038
13114
|
var AgentOrchestrator = class {
|
|
13039
13115
|
harness;
|
|
13040
13116
|
conversationStore;
|
|
@@ -13478,6 +13554,7 @@ var AgentOrchestrator = class {
|
|
|
13478
13554
|
subagentId,
|
|
13479
13555
|
task: conv.subagentMeta?.task ?? conv.title,
|
|
13480
13556
|
status: "completed",
|
|
13557
|
+
taskOutcome: deriveTaskOutcome(responseText, false),
|
|
13481
13558
|
result: { status: "completed", response: responseText, steps: 0, tokens: { input: 0, output: 0, cached: 0 }, duration: 0 },
|
|
13482
13559
|
timestamp: Date.now()
|
|
13483
13560
|
};
|
|
@@ -13579,7 +13656,11 @@ var AgentOrchestrator = class {
|
|
|
13579
13656
|
const harnessMessages = [...runOutcome.messages];
|
|
13580
13657
|
const recallParams = this.hooks?.buildRecallParams?.({ ownerId, tenantId: conversation.tenantId, excludeConversationId: childConversationId }) ?? {};
|
|
13581
13658
|
for await (const event of childHarness.runWithTelemetry({
|
|
13582
|
-
task
|
|
13659
|
+
// Ask the subagent to self-report a machine-readable task outcome at the
|
|
13660
|
+
// end of its final message; `deriveTaskOutcome` parses it so a failed
|
|
13661
|
+
// task can never be recorded as "completed". Appended only to the model
|
|
13662
|
+
// input, not the stored/displayed task name.
|
|
13663
|
+
task: `${task}${SUBAGENT_OUTCOME_INSTRUCTION}`,
|
|
13583
13664
|
conversationId: childConversationId,
|
|
13584
13665
|
tenantId: conversation.tenantId ?? void 0,
|
|
13585
13666
|
parameters: withToolResultArchiveParam({
|
|
@@ -13766,11 +13847,14 @@ var AgentOrchestrator = class {
|
|
|
13766
13847
|
if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
|
|
13767
13848
|
}
|
|
13768
13849
|
const abnormal = !runResult;
|
|
13769
|
-
const
|
|
13850
|
+
const taskOutcome = deriveTaskOutcome(gathered, abnormal);
|
|
13851
|
+
const cleanedGathered = stripOutcomeVerdict(gathered);
|
|
13852
|
+
const subagentResponse = abnormal ? abnormalEndResponse({ subagentId: childConversationId, gathered: cleanedGathered, runError }) : cleanedGathered;
|
|
13770
13853
|
const pendingResult = {
|
|
13771
13854
|
subagentId: childConversationId,
|
|
13772
13855
|
task,
|
|
13773
13856
|
status: abnormal ? "error" : "completed",
|
|
13857
|
+
taskOutcome,
|
|
13774
13858
|
result: {
|
|
13775
13859
|
status: runResult?.status ?? "error",
|
|
13776
13860
|
response: subagentResponse,
|
|
@@ -13811,6 +13895,7 @@ var AgentOrchestrator = class {
|
|
|
13811
13895
|
subagentId: childConversationId,
|
|
13812
13896
|
task,
|
|
13813
13897
|
status: "error",
|
|
13898
|
+
taskOutcome: "failed",
|
|
13814
13899
|
error: { code: "SUBAGENT_ERROR", message: errMsg },
|
|
13815
13900
|
timestamp: Date.now()
|
|
13816
13901
|
};
|
|
@@ -13878,10 +13963,10 @@ Response: ${responseLine}
|
|
|
13878
13963
|
Steps: ${pr.result.steps}, Duration: ${pr.result.duration}ms` : pr.error ? `Error: ${pr.error.message}` : "(no result)";
|
|
13879
13964
|
const injected = {
|
|
13880
13965
|
role: "user",
|
|
13881
|
-
content: `[Subagent Result] Subagent "${pr.task}" (${pr.subagentId}) ${pr.
|
|
13966
|
+
content: `[Subagent Result] Subagent "${pr.task}" (${pr.subagentId}) ${pr.taskOutcome}:
|
|
13882
13967
|
|
|
13883
13968
|
${resultBody}`,
|
|
13884
|
-
metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, timestamp: pr.timestamp }
|
|
13969
|
+
metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, taskOutcome: pr.taskOutcome, timestamp: pr.timestamp }
|
|
13885
13970
|
};
|
|
13886
13971
|
injectedCallbackMessages.push(injected);
|
|
13887
13972
|
conversation.messages.push(injected);
|
|
@@ -14164,13 +14249,16 @@ ${resultBody}`,
|
|
|
14164
14249
|
if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
|
|
14165
14250
|
}
|
|
14166
14251
|
const abnormal = !runResult;
|
|
14167
|
-
const
|
|
14252
|
+
const taskOutcome = deriveTaskOutcome(gathered, abnormal);
|
|
14253
|
+
const cleanedGathered = stripOutcomeVerdict(gathered);
|
|
14254
|
+
const subagentResponse = abnormal ? abnormalEndResponse({ subagentId: conversationId, gathered: cleanedGathered, runError }) : cleanedGathered;
|
|
14168
14255
|
const parentConv = await this.conversationStore.get(parentConversationId);
|
|
14169
14256
|
if (parentConv) {
|
|
14170
14257
|
const result = {
|
|
14171
14258
|
subagentId: conversationId,
|
|
14172
14259
|
task,
|
|
14173
14260
|
status: abnormal ? "error" : "completed",
|
|
14261
|
+
taskOutcome,
|
|
14174
14262
|
result: { status: runResult?.status ?? "error", response: subagentResponse, steps: runResult?.steps ?? 0, tokens: { input: 0, output: 0, cached: 0 }, duration: runResult?.duration ?? 0 },
|
|
14175
14263
|
...abnormal ? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } } : {},
|
|
14176
14264
|
timestamp: Date.now()
|
|
@@ -14219,6 +14307,7 @@ ${resultBody}`,
|
|
|
14219
14307
|
subagentId: conversationId,
|
|
14220
14308
|
task,
|
|
14221
14309
|
status: "error",
|
|
14310
|
+
taskOutcome: "failed",
|
|
14222
14311
|
error: { code: "CONTINUATION_ERROR", message: err instanceof Error ? err.message : String(err) },
|
|
14223
14312
|
timestamp: Date.now()
|
|
14224
14313
|
};
|
|
@@ -14462,6 +14551,7 @@ ${resultBody}`,
|
|
|
14462
14551
|
subagentId: childConversationId,
|
|
14463
14552
|
task,
|
|
14464
14553
|
status: "error",
|
|
14554
|
+
taskOutcome: "failed",
|
|
14465
14555
|
error: { code: "SUBAGENT_SPAWN_FAILED", message },
|
|
14466
14556
|
timestamp: Date.now()
|
|
14467
14557
|
});
|
|
@@ -14493,6 +14583,7 @@ ${resultBody}`,
|
|
|
14493
14583
|
subagentId: conv.conversationId,
|
|
14494
14584
|
task: conv.subagentMeta.task,
|
|
14495
14585
|
status: "error",
|
|
14586
|
+
taskOutcome: "failed",
|
|
14496
14587
|
error: conv.subagentMeta.error,
|
|
14497
14588
|
timestamp: Date.now()
|
|
14498
14589
|
};
|
|
@@ -14635,6 +14726,7 @@ var runConversationTurn = async (opts) => {
|
|
|
14635
14726
|
disablePromptCache: opts.disablePromptCache,
|
|
14636
14727
|
suppressTelemetry: opts.suppressTelemetry,
|
|
14637
14728
|
model: opts.model,
|
|
14729
|
+
maxSteps: opts.maxSteps,
|
|
14638
14730
|
volatileContext: opts.volatileContext,
|
|
14639
14731
|
telemetryAttributes: opts.telemetryAttributes
|
|
14640
14732
|
},
|
|
@@ -14979,6 +15071,7 @@ export {
|
|
|
14979
15071
|
estimateTotalTokens,
|
|
14980
15072
|
executeConversationTurn,
|
|
14981
15073
|
findSafeSplitPoint,
|
|
15074
|
+
findSafeSplitPointByTurns,
|
|
14982
15075
|
flushTurnDraft,
|
|
14983
15076
|
generateAgentId,
|
|
14984
15077
|
getAgentStoreDirectory,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poncho-ai/harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
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
|
};
|
package/src/compaction.ts
CHANGED
|
@@ -9,22 +9,36 @@ const MIN_COMPACTABLE_MESSAGES = 4;
|
|
|
9
9
|
const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
|
|
10
10
|
enabled: true,
|
|
11
11
|
trigger: 0.75,
|
|
12
|
-
|
|
12
|
+
keepRecentTurns: 4,
|
|
13
|
+
keepRecentMessages: 6,
|
|
13
14
|
};
|
|
14
|
-
const SUMMARIZATION_MESSAGE_TRUNCATION_CHARS =
|
|
15
|
-
const SUMMARIZATION_MAX_OUTPUT_TOKENS =
|
|
15
|
+
const SUMMARIZATION_MESSAGE_TRUNCATION_CHARS = 4000;
|
|
16
|
+
const SUMMARIZATION_MAX_OUTPUT_TOKENS = 8192;
|
|
17
|
+
/**
|
|
18
|
+
* Upper bound on the tokens fed INTO the summarization model call. With hundreds
|
|
19
|
+
* of compacted messages at 4000 chars each the joined transcript can exceed the
|
|
20
|
+
* summarizer's own window; we drop the oldest non-error, non-prior-summary
|
|
21
|
+
* messages first so recent context and every failure survive. Held well under a
|
|
22
|
+
* 200k window to leave room for the prompt + output.
|
|
23
|
+
*/
|
|
24
|
+
const SUMMARIZATION_MAX_INPUT_TOKENS = 120_000;
|
|
16
25
|
|
|
17
26
|
const SUMMARIZATION_PROMPT = `Summarize the following conversation into a structured working state that allows continuation without re-asking questions. Include:
|
|
18
27
|
|
|
19
28
|
1. **User intent**: What the user originally asked for and any refinements
|
|
20
|
-
2. **Completed work**: What has been accomplished so far
|
|
29
|
+
2. **Completed work**: What has been accomplished AND CONFIRMED so far
|
|
21
30
|
3. **Key decisions**: Technical decisions made and their rationale
|
|
22
|
-
4. **
|
|
23
|
-
5. **
|
|
24
|
-
6. **
|
|
31
|
+
4. **Unresolved errors & failures**: Every error, failed or uncertain tool call, and failed or incomplete subagent — 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.
|
|
32
|
+
5. **Pending promises**: Anything the assistant said it WOULD do but has not confirmed done.
|
|
33
|
+
6. **Referenced resources**: Files, URLs, tools, or data referenced
|
|
34
|
+
7. **Pending next steps**: What remains to be done
|
|
35
|
+
|
|
36
|
+
Rules:
|
|
37
|
+
- 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.
|
|
38
|
+
- Preserve identifiers verbatim — file paths, URLs, IDs, command names, tool names. Do not paraphrase or truncate them.
|
|
25
39
|
|
|
26
40
|
Be concise but preserve all information needed to continue the task.
|
|
27
|
-
Omit any section that has no relevant content.`;
|
|
41
|
+
Omit any section that has no relevant content, EXCEPT "Unresolved errors & failures" when a failure occurred.`;
|
|
28
42
|
|
|
29
43
|
/**
|
|
30
44
|
* Extra instruction appended when the first compacted message is itself a
|
|
@@ -34,8 +48,13 @@ Omit any section that has no relevant content.`;
|
|
|
34
48
|
*/
|
|
35
49
|
const 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 — only drop it if the newer messages explicitly supersede it.`;
|
|
36
50
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Max chars of a subagent result text kept verbatim in the ledger digest. Kept
|
|
53
|
+
* large (and NOT gated on outcome) so a failed subagent's own explanation
|
|
54
|
+
* survives compaction — the failing case is exactly the one that must not be
|
|
55
|
+
* clipped, and it can be mislabeled, so gating on status would be self-defeating.
|
|
56
|
+
*/
|
|
57
|
+
const SUBAGENT_DIGEST_CHARS = 2000;
|
|
39
58
|
|
|
40
59
|
/** Heading used for the verbatim, model-proof subagent ledger block. */
|
|
41
60
|
const SUBAGENT_LEDGER_HEADING = "## Subagents";
|
|
@@ -47,6 +66,8 @@ export const resolveCompactionConfig = (
|
|
|
47
66
|
return {
|
|
48
67
|
enabled: explicit.enabled ?? DEFAULT_COMPACTION_CONFIG.enabled,
|
|
49
68
|
trigger: explicit.trigger ?? DEFAULT_COMPACTION_CONFIG.trigger,
|
|
69
|
+
keepRecentTurns:
|
|
70
|
+
explicit.keepRecentTurns ?? DEFAULT_COMPACTION_CONFIG.keepRecentTurns,
|
|
50
71
|
keepRecentMessages:
|
|
51
72
|
explicit.keepRecentMessages ??
|
|
52
73
|
DEFAULT_COMPACTION_CONFIG.keepRecentMessages,
|
|
@@ -162,6 +183,71 @@ export const findSafeSplitPoint = (
|
|
|
162
183
|
return -1;
|
|
163
184
|
};
|
|
164
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Find the split index that preserves the last `keepRecentTurns` whole *turns*
|
|
188
|
+
* verbatim. A turn begins at a `role:"user"` message and runs until the next
|
|
189
|
+
* `user` message. Everything before the split is folded into the summary;
|
|
190
|
+
* everything from the split onward is kept as-is.
|
|
191
|
+
*
|
|
192
|
+
* Two guards make a candidate split acceptable:
|
|
193
|
+
* 1. The compacted side has at least `MIN_COMPACTABLE_MESSAGES` messages.
|
|
194
|
+
* 2. The preserved side estimates at most `maxPreservedTokens` — so keeping N
|
|
195
|
+
* large turns can never leave the post-compaction context above the
|
|
196
|
+
* compaction trigger (which caused re-compaction thrash / window overflow).
|
|
197
|
+
* The tool-call-orphan guard (`splitOrphansToolCalls`) is preserved: a split
|
|
198
|
+
* that would strand an assistant tool-call on the compacted side steps to an
|
|
199
|
+
* earlier user boundary (which only preserves more, always safe).
|
|
200
|
+
*
|
|
201
|
+
* We try the largest N (up to `keepRecentTurns`) first and decrement, so we
|
|
202
|
+
* keep as many turns as fit. If no turn boundary yields a safe split (e.g. a
|
|
203
|
+
* single giant turn near the window), fall back to the message-based
|
|
204
|
+
* `findSafeSplitPoint` so the middle of that turn still compacts.
|
|
205
|
+
*
|
|
206
|
+
* Returns -1 when nothing safe can be compacted.
|
|
207
|
+
*/
|
|
208
|
+
export const findSafeSplitPointByTurns = (
|
|
209
|
+
messages: Message[],
|
|
210
|
+
keepRecentTurns: number,
|
|
211
|
+
keepRecentMessagesFallback: number,
|
|
212
|
+
maxPreservedTokens: number,
|
|
213
|
+
): number => {
|
|
214
|
+
// Indices of every user message, in order.
|
|
215
|
+
const userIdx: number[] = [];
|
|
216
|
+
for (let i = 0; i < messages.length; i++) {
|
|
217
|
+
if (messages[i]!.role === "user") userIdx.push(i);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const maxN = Math.min(keepRecentTurns, userIdx.length);
|
|
221
|
+
for (let n = maxN; n >= 1; n--) {
|
|
222
|
+
// The Nth-from-last user message starts the preserved region.
|
|
223
|
+
let guardN = n;
|
|
224
|
+
let split = userIdx[userIdx.length - guardN]!;
|
|
225
|
+
// Orphan guard: step to an earlier user boundary if the compacted side
|
|
226
|
+
// would end on an assistant tool-call whose result moved to the preserved
|
|
227
|
+
// side. Walking earlier only preserves more, so it stays within budget
|
|
228
|
+
// checks below.
|
|
229
|
+
while (
|
|
230
|
+
splitOrphansToolCalls(messages, split) &&
|
|
231
|
+
guardN < userIdx.length
|
|
232
|
+
) {
|
|
233
|
+
guardN++;
|
|
234
|
+
split = userIdx[userIdx.length - guardN]!;
|
|
235
|
+
}
|
|
236
|
+
if (split < MIN_COMPACTABLE_MESSAGES) continue;
|
|
237
|
+
const preservedTokens = estimateTokens(
|
|
238
|
+
messages
|
|
239
|
+
.slice(split)
|
|
240
|
+
.map((m) => getTextContent(m))
|
|
241
|
+
.join("\n"),
|
|
242
|
+
);
|
|
243
|
+
if (preservedTokens <= maxPreservedTokens) return split;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// No turn boundary fits: fall back to the legacy message-based split so a
|
|
247
|
+
// single giant turn still gets its middle compacted.
|
|
248
|
+
return findSafeSplitPoint(messages, keepRecentMessagesFallback);
|
|
249
|
+
};
|
|
250
|
+
|
|
165
251
|
/**
|
|
166
252
|
* Whether a message is itself a prior compaction summary.
|
|
167
253
|
*/
|
|
@@ -175,8 +261,17 @@ const isCompactionSummary = (msg: Message): boolean =>
|
|
|
175
261
|
* compaction summary, it is passed in FULL (not truncated to
|
|
176
262
|
* SUMMARIZATION_MESSAGE_TRUNCATION_CHARS) and tagged `[prior-summary]`, and
|
|
177
263
|
* the prompt instructs the model to merge-and-update rather than
|
|
178
|
-
* re-summarize. All other messages keep the
|
|
264
|
+
* re-summarize. All other messages keep the per-message truncation.
|
|
265
|
+
*
|
|
266
|
+
* Total-input budget: with many messages the joined transcript can exceed the
|
|
267
|
+
* summarizer's own window, so we cap it at SUMMARIZATION_MAX_INPUT_TOKENS and
|
|
268
|
+
* drop the OLDEST non-important lines first. A line is "important" (never
|
|
269
|
+
* dropped) if it is the prior summary or carries a failure/error signal — so
|
|
270
|
+
* every failure and the most recent context always reach the summarizer.
|
|
179
271
|
*/
|
|
272
|
+
const FAILURE_SIGNAL_RE =
|
|
273
|
+
/\berror\b|\bfailed\b|\bfailure\b|\bexception\b|could ?n[’']?t|cannot\b|unable to|\[subagent result\]/i;
|
|
274
|
+
|
|
180
275
|
const buildSummarizationMessages = (
|
|
181
276
|
messagesToCompact: Message[],
|
|
182
277
|
instructions?: string,
|
|
@@ -184,7 +279,12 @@ const buildSummarizationMessages = (
|
|
|
184
279
|
const hasPriorSummary =
|
|
185
280
|
messagesToCompact.length > 0 && isCompactionSummary(messagesToCompact[0]!);
|
|
186
281
|
|
|
187
|
-
|
|
282
|
+
interface Line {
|
|
283
|
+
text: string;
|
|
284
|
+
tokens: number;
|
|
285
|
+
important: boolean;
|
|
286
|
+
}
|
|
287
|
+
const lines: Line[] = [];
|
|
188
288
|
for (let i = 0; i < messagesToCompact.length; i++) {
|
|
189
289
|
const msg = messagesToCompact[i]!;
|
|
190
290
|
const text = getTextContent(msg);
|
|
@@ -196,9 +296,29 @@ const buildSummarizationMessages = (
|
|
|
196
296
|
: text.slice(0, SUMMARIZATION_MESSAGE_TRUNCATION_CHARS) +
|
|
197
297
|
"\n...[truncated]";
|
|
198
298
|
const tag = isPrior ? "prior-summary" : msg.role;
|
|
199
|
-
|
|
299
|
+
const line = `[${tag}]: ${rendered}`;
|
|
300
|
+
lines.push({
|
|
301
|
+
text: line,
|
|
302
|
+
tokens: estimateTokens(line),
|
|
303
|
+
important: isPrior || FAILURE_SIGNAL_RE.test(text),
|
|
304
|
+
});
|
|
200
305
|
}
|
|
201
306
|
|
|
307
|
+
// Enforce the input budget: while over, drop the oldest non-important line.
|
|
308
|
+
let total = lines.reduce((sum, l) => sum + l.tokens, 0);
|
|
309
|
+
if (total > SUMMARIZATION_MAX_INPUT_TOKENS) {
|
|
310
|
+
for (let i = 0; i < lines.length && total > SUMMARIZATION_MAX_INPUT_TOKENS; ) {
|
|
311
|
+
if (lines[i]!.important) {
|
|
312
|
+
i++;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
total -= lines[i]!.tokens;
|
|
316
|
+
lines.splice(i, 1);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const conversationLines = lines.map((l) => l.text);
|
|
321
|
+
|
|
202
322
|
let prompt = SUMMARIZATION_PROMPT;
|
|
203
323
|
if (hasPriorSummary) prompt = `${prompt}\n\n${CUMULATIVE_SUMMARY_PROMPT}`;
|
|
204
324
|
if (instructions) prompt = `${prompt}\n\nAdditional focus: ${instructions}`;
|
|
@@ -246,7 +366,15 @@ const parseSubagentCallback = (msg: Message): SubagentLedgerEntry | null => {
|
|
|
246
366
|
typeof meta.task === "string" && meta.task
|
|
247
367
|
? meta.task
|
|
248
368
|
: headerMatch?.[1] ?? "";
|
|
249
|
-
|
|
369
|
+
// The ledger "status" is the subagent's TASK OUTCOME (succeeded/failed/
|
|
370
|
+
// partial/unknown), not its run status. Prefer the structured metadata; fall
|
|
371
|
+
// back to the header token. NEVER default to "completed" — an unparseable
|
|
372
|
+
// header must not silently assert success. `status` remains the field name
|
|
373
|
+
// for backward compat with parsePriorLedger's rendered shape.
|
|
374
|
+
const status =
|
|
375
|
+
(typeof meta.taskOutcome === "string" && meta.taskOutcome) ||
|
|
376
|
+
headerMatch?.[3] ||
|
|
377
|
+
"unknown";
|
|
250
378
|
|
|
251
379
|
// Digest = the body after the header line (the result text), capped.
|
|
252
380
|
const bodyStart = text.indexOf("\n\n");
|
|
@@ -338,8 +466,17 @@ const buildContinuationMessage = (summary: string): Message => ({
|
|
|
338
466
|
|
|
339
467
|
export interface CompactMessagesOptions {
|
|
340
468
|
instructions?: string;
|
|
469
|
+
/**
|
|
470
|
+
* The model's context window (tokens). Used to bound the preserved side of a
|
|
471
|
+
* turn-based split so keeping N recent turns can't leave the post-compaction
|
|
472
|
+
* context above the compaction trigger. Defaults to 200k when omitted.
|
|
473
|
+
*/
|
|
474
|
+
contextWindow?: number;
|
|
341
475
|
}
|
|
342
476
|
|
|
477
|
+
/** Fraction of the context window the preserved (verbatim) side may occupy. */
|
|
478
|
+
const MAX_PRESERVED_CONTEXT_FRACTION = 0.5;
|
|
479
|
+
|
|
343
480
|
export interface CompactResult {
|
|
344
481
|
compacted: boolean;
|
|
345
482
|
messages: Message[];
|
|
@@ -363,7 +500,18 @@ export const compactMessages = async (
|
|
|
363
500
|
config: CompactionConfig,
|
|
364
501
|
options?: CompactMessagesOptions,
|
|
365
502
|
): Promise<CompactResult> => {
|
|
366
|
-
const
|
|
503
|
+
const contextWindow = options?.contextWindow && options.contextWindow > 0
|
|
504
|
+
? options.contextWindow
|
|
505
|
+
: 200_000;
|
|
506
|
+
const maxPreservedTokens = Math.floor(
|
|
507
|
+
contextWindow * MAX_PRESERVED_CONTEXT_FRACTION,
|
|
508
|
+
);
|
|
509
|
+
const splitIdx = findSafeSplitPointByTurns(
|
|
510
|
+
messages,
|
|
511
|
+
config.keepRecentTurns,
|
|
512
|
+
config.keepRecentMessages,
|
|
513
|
+
maxPreservedTokens,
|
|
514
|
+
);
|
|
367
515
|
if (splitIdx === -1) {
|
|
368
516
|
return {
|
|
369
517
|
compacted: false,
|
package/src/harness.ts
CHANGED
|
@@ -2286,7 +2286,12 @@ export class AgentHarness {
|
|
|
2286
2286
|
const modelName = agent.frontmatter.model?.name ?? "claude-opus-4-5";
|
|
2287
2287
|
const modelInstance = this.modelProvider(modelName);
|
|
2288
2288
|
const config = resolveCompactionConfig(agent.frontmatter.compaction);
|
|
2289
|
-
|
|
2289
|
+
const contextWindow =
|
|
2290
|
+
agent.frontmatter.model?.contextWindow ?? getModelContextWindow(modelName);
|
|
2291
|
+
return compactMessages(modelInstance, messages, config, {
|
|
2292
|
+
...options,
|
|
2293
|
+
contextWindow: options?.contextWindow ?? contextWindow,
|
|
2294
|
+
});
|
|
2290
2295
|
}
|
|
2291
2296
|
|
|
2292
2297
|
async *run(input: RunInput): AsyncGenerator<AgentEvent> {
|
|
@@ -2317,7 +2322,7 @@ export class AgentHarness {
|
|
|
2317
2322
|
let agent = this.parsedAgent as ParsedAgent;
|
|
2318
2323
|
const runId = `run_${randomUUID()}`;
|
|
2319
2324
|
const start = now();
|
|
2320
|
-
const maxSteps = agent.frontmatter.limits?.maxSteps ?? 20;
|
|
2325
|
+
const maxSteps = input.maxSteps ?? agent.frontmatter.limits?.maxSteps ?? 20;
|
|
2321
2326
|
// A constructor-level override (e.g. a longer budget for background
|
|
2322
2327
|
// subagents) takes precedence over the agent definition's limits.timeout.
|
|
2323
2328
|
const configuredTimeout = this.runTimeoutSecOverride ?? agent.frontmatter.limits?.timeout;
|
|
@@ -3082,6 +3087,7 @@ Code is wrapped in an async IIFE — use \`return\` to return a value to the too
|
|
|
3082
3087
|
modelInstance,
|
|
3083
3088
|
messages,
|
|
3084
3089
|
compactionConfig,
|
|
3090
|
+
{ contextWindow },
|
|
3085
3091
|
);
|
|
3086
3092
|
if (compactResult.compacted) {
|
|
3087
3093
|
messages.length = 0;
|
|
@@ -3096,6 +3102,16 @@ Code is wrapped in an async IIFE — use \`return\` to return a value to the too
|
|
|
3096
3102
|
emittedMessages.pop();
|
|
3097
3103
|
}
|
|
3098
3104
|
}
|
|
3105
|
+
// MID-RUN COMPACTION (step > 1) intentionally emits
|
|
3106
|
+
// `compactedMessages: undefined`. The live `messages` array is
|
|
3107
|
+
// already rewritten in place above (so the rest of the run and
|
|
3108
|
+
// the end-of-run `continuationMessages` reflect the compaction),
|
|
3109
|
+
// but we do NOT drive the runner's persisted `compactedHistory`
|
|
3110
|
+
// archive mid-turn: that would require a get→mutate→update on the
|
|
3111
|
+
// conversation row while the turn is still in flight, which is the
|
|
3112
|
+
// clobber pattern PonchOS forbids for non-terminal writers. The
|
|
3113
|
+
// compacted content is preserved in the summary; only the raw
|
|
3114
|
+
// pre-compaction transcript archive skips these mid-run events.
|
|
3099
3115
|
const tokensAfterCompaction = estimateTotalTokens(systemPrompt, messages, toolDefsJsonForEstimate);
|
|
3100
3116
|
latestContextTokens = tokensAfterCompaction;
|
|
3101
3117
|
toolOutputEstimateSinceModel = 0;
|
|
@@ -4109,6 +4125,13 @@ Code is wrapped in an async IIFE — use \`return\` to return a value to the too
|
|
|
4109
4125
|
* otherwise the continuation falls back to the agent definition's
|
|
4110
4126
|
* (possibly concurrently-mutated) frontmatter model. */
|
|
4111
4127
|
model?: string;
|
|
4128
|
+
/** Same as `RunInput.volatileContext`. Pass it explicitly on resume —
|
|
4129
|
+
* the per-conversation capture covers same-process continuations, but
|
|
4130
|
+
* a checkpoint resumed after a restart would otherwise lose the
|
|
4131
|
+
* embedder's volatile blocks. */
|
|
4132
|
+
volatileContext?: string;
|
|
4133
|
+
/** Same as `RunInput.telemetryAttributes`, for the continuation run. */
|
|
4134
|
+
telemetryAttributes?: Record<string, string>;
|
|
4112
4135
|
}): AsyncGenerator<AgentEvent> {
|
|
4113
4136
|
const messages = [...input.messages];
|
|
4114
4137
|
const lastMsg = messages[messages.length - 1];
|
|
@@ -4169,6 +4192,8 @@ Code is wrapped in an async IIFE — use \`return\` to return a value to the too
|
|
|
4169
4192
|
abortSignal: input.abortSignal,
|
|
4170
4193
|
suppressTelemetry: input.suppressTelemetry,
|
|
4171
4194
|
model: input.model,
|
|
4195
|
+
volatileContext: input.volatileContext,
|
|
4196
|
+
telemetryAttributes: input.telemetryAttributes,
|
|
4172
4197
|
});
|
|
4173
4198
|
}
|
|
4174
4199
|
|
|
@@ -107,6 +107,48 @@ export const abnormalEndResponse = (opts: {
|
|
|
107
107
|
return opts.gathered ? `${head} ${recover}\n\n${opts.gathered}` : `${head} ${recover}`;
|
|
108
108
|
};
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Machine-readable verdict a subagent is asked to append to its final message,
|
|
112
|
+
* e.g. `[[OUTCOME: failed]] reason`. Parsed deterministically — see
|
|
113
|
+
* SUBAGENT_OUTCOME_INSTRUCTION and `deriveTaskOutcome`.
|
|
114
|
+
*/
|
|
115
|
+
const SUBAGENT_OUTCOME_RE = /\[\[\s*OUTCOME\s*:\s*(succeeded|failed|partial)\s*\]\]/i;
|
|
116
|
+
|
|
117
|
+
/** Appended to a subagent's task so it self-reports whether it succeeded. */
|
|
118
|
+
export const SUBAGENT_OUTCOME_INSTRUCTION =
|
|
119
|
+
"\n\nAt the very end of your final message, on its own line, append a " +
|
|
120
|
+
"machine-readable outcome verdict for the task you were given: " +
|
|
121
|
+
"`[[OUTCOME: succeeded]]`, `[[OUTCOME: partial]]`, or `[[OUTCOME: failed]]`, " +
|
|
122
|
+
"followed by a one-line reason. Judge honestly by whether YOU actually " +
|
|
123
|
+
"accomplished the task (produced the real result / wrote the files), not by " +
|
|
124
|
+
"whether the run finished. If a tool or capability you needed was missing, " +
|
|
125
|
+
"that is `failed`.";
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Derive a subagent's task outcome (distinct from run status). Abnormal ends and
|
|
129
|
+
* empty output are `failed`; otherwise we parse the self-declared verdict and
|
|
130
|
+
* default to `unknown` when absent (never assume success).
|
|
131
|
+
*/
|
|
132
|
+
export const deriveTaskOutcome = (
|
|
133
|
+
gathered: string,
|
|
134
|
+
abnormal: boolean,
|
|
135
|
+
): import("../state.js").SubagentTaskOutcome => {
|
|
136
|
+
if (abnormal) return "failed";
|
|
137
|
+
if (!gathered.trim()) return "failed";
|
|
138
|
+
const m = gathered.match(SUBAGENT_OUTCOME_RE);
|
|
139
|
+
if (!m) return "unknown";
|
|
140
|
+
const v = m[1]!.toLowerCase();
|
|
141
|
+
return v === "succeeded" ? "succeeded" : v === "partial" ? "partial" : "failed";
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Remove the machine-readable `[[OUTCOME: …]]` verdict (and its one-line reason)
|
|
146
|
+
* from the text delivered to the parent — the outcome is carried structurally in
|
|
147
|
+
* the callback header/metadata, so the marker shouldn't leak into chat UI.
|
|
148
|
+
*/
|
|
149
|
+
export const stripOutcomeVerdict = (text: string): string =>
|
|
150
|
+
text.replace(/\n*\[\[\s*OUTCOME\s*:\s*(?:succeeded|failed|partial)\s*\]\].*$/is, "").trimEnd();
|
|
151
|
+
|
|
110
152
|
// ── Types ──
|
|
111
153
|
|
|
112
154
|
export type ActiveConversationRun = {
|
|
@@ -697,6 +739,7 @@ export class AgentOrchestrator {
|
|
|
697
739
|
subagentId,
|
|
698
740
|
task: conv.subagentMeta?.task ?? conv.title,
|
|
699
741
|
status: "completed",
|
|
742
|
+
taskOutcome: deriveTaskOutcome(responseText, false),
|
|
700
743
|
result: { status: "completed", response: responseText, steps: 0, tokens: { input: 0, output: 0, cached: 0 }, duration: 0 },
|
|
701
744
|
timestamp: Date.now(),
|
|
702
745
|
};
|
|
@@ -829,7 +872,11 @@ export class AgentOrchestrator {
|
|
|
829
872
|
const recallParams = this.hooks?.buildRecallParams?.({ ownerId, tenantId: conversation.tenantId, excludeConversationId: childConversationId }) ?? {};
|
|
830
873
|
|
|
831
874
|
for await (const event of childHarness.runWithTelemetry({
|
|
832
|
-
task
|
|
875
|
+
// Ask the subagent to self-report a machine-readable task outcome at the
|
|
876
|
+
// end of its final message; `deriveTaskOutcome` parses it so a failed
|
|
877
|
+
// task can never be recorded as "completed". Appended only to the model
|
|
878
|
+
// input, not the stored/displayed task name.
|
|
879
|
+
task: `${task}${SUBAGENT_OUTCOME_INSTRUCTION}`,
|
|
833
880
|
conversationId: childConversationId,
|
|
834
881
|
tenantId: conversation.tenantId ?? undefined,
|
|
835
882
|
parameters: withToolResultArchiveParam({
|
|
@@ -1040,13 +1087,16 @@ export class AgentOrchestrator {
|
|
|
1040
1087
|
// the work — deliver what it gathered, tagged so the parent knows it
|
|
1041
1088
|
// didn't finish, and build a result so it never renders as "(no result)".
|
|
1042
1089
|
const abnormal = !runResult;
|
|
1090
|
+
const taskOutcome = deriveTaskOutcome(gathered, abnormal);
|
|
1091
|
+
const cleanedGathered = stripOutcomeVerdict(gathered);
|
|
1043
1092
|
const subagentResponse = abnormal
|
|
1044
|
-
? abnormalEndResponse({ subagentId: childConversationId, gathered, runError })
|
|
1045
|
-
:
|
|
1093
|
+
? abnormalEndResponse({ subagentId: childConversationId, gathered: cleanedGathered, runError })
|
|
1094
|
+
: cleanedGathered;
|
|
1046
1095
|
const pendingResult: PendingSubagentResult = {
|
|
1047
1096
|
subagentId: childConversationId,
|
|
1048
1097
|
task,
|
|
1049
1098
|
status: abnormal ? "error" : "completed",
|
|
1099
|
+
taskOutcome,
|
|
1050
1100
|
result: {
|
|
1051
1101
|
status: runResult?.status ?? "error",
|
|
1052
1102
|
response: subagentResponse,
|
|
@@ -1096,6 +1146,7 @@ export class AgentOrchestrator {
|
|
|
1096
1146
|
subagentId: childConversationId,
|
|
1097
1147
|
task,
|
|
1098
1148
|
status: "error",
|
|
1149
|
+
taskOutcome: "failed",
|
|
1099
1150
|
error: { code: "SUBAGENT_ERROR", message: errMsg },
|
|
1100
1151
|
timestamp: Date.now(),
|
|
1101
1152
|
};
|
|
@@ -1185,8 +1236,8 @@ export class AgentOrchestrator {
|
|
|
1185
1236
|
: "(no result)";
|
|
1186
1237
|
const injected: Message = {
|
|
1187
1238
|
role: "user",
|
|
1188
|
-
content: `[Subagent Result] Subagent "${pr.task}" (${pr.subagentId}) ${pr.
|
|
1189
|
-
metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, timestamp: pr.timestamp } as Message["metadata"],
|
|
1239
|
+
content: `[Subagent Result] Subagent "${pr.task}" (${pr.subagentId}) ${pr.taskOutcome}:\n\n${resultBody}`,
|
|
1240
|
+
metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, taskOutcome: pr.taskOutcome, timestamp: pr.timestamp } as Message["metadata"],
|
|
1190
1241
|
};
|
|
1191
1242
|
injectedCallbackMessages.push(injected);
|
|
1192
1243
|
conversation.messages.push(injected);
|
|
@@ -1516,9 +1567,11 @@ export class AgentOrchestrator {
|
|
|
1516
1567
|
if (freshSubConv) gathered = realResponseText(lastAssistantText(freshSubConv.messages));
|
|
1517
1568
|
}
|
|
1518
1569
|
const abnormal = !runResult;
|
|
1570
|
+
const taskOutcome = deriveTaskOutcome(gathered, abnormal);
|
|
1571
|
+
const cleanedGathered = stripOutcomeVerdict(gathered);
|
|
1519
1572
|
const subagentResponse = abnormal
|
|
1520
|
-
? abnormalEndResponse({ subagentId: conversationId, gathered, runError })
|
|
1521
|
-
:
|
|
1573
|
+
? abnormalEndResponse({ subagentId: conversationId, gathered: cleanedGathered, runError })
|
|
1574
|
+
: cleanedGathered;
|
|
1522
1575
|
|
|
1523
1576
|
const parentConv = await this.conversationStore.get(parentConversationId);
|
|
1524
1577
|
if (parentConv) {
|
|
@@ -1526,6 +1579,7 @@ export class AgentOrchestrator {
|
|
|
1526
1579
|
subagentId: conversationId,
|
|
1527
1580
|
task,
|
|
1528
1581
|
status: abnormal ? "error" : "completed",
|
|
1582
|
+
taskOutcome,
|
|
1529
1583
|
result: { status: runResult?.status ?? "error", response: subagentResponse, steps: runResult?.steps ?? 0, tokens: { input: 0, output: 0, cached: 0 }, duration: runResult?.duration ?? 0 },
|
|
1530
1584
|
...(abnormal
|
|
1531
1585
|
? { error: { code: runError?.code ?? "SUBAGENT_INCOMPLETE", message: runError?.message ?? "subagent ended without a result" } }
|
|
@@ -1576,6 +1630,7 @@ export class AgentOrchestrator {
|
|
|
1576
1630
|
subagentId: conversationId,
|
|
1577
1631
|
task,
|
|
1578
1632
|
status: "error",
|
|
1633
|
+
taskOutcome: "failed",
|
|
1579
1634
|
error: { code: "CONTINUATION_ERROR", message: err instanceof Error ? err.message : String(err) },
|
|
1580
1635
|
timestamp: Date.now(),
|
|
1581
1636
|
};
|
|
@@ -1858,6 +1913,7 @@ export class AgentOrchestrator {
|
|
|
1858
1913
|
subagentId: childConversationId,
|
|
1859
1914
|
task,
|
|
1860
1915
|
status: "error",
|
|
1916
|
+
taskOutcome: "failed",
|
|
1861
1917
|
error: { code: "SUBAGENT_SPAWN_FAILED", message },
|
|
1862
1918
|
timestamp: Date.now(),
|
|
1863
1919
|
});
|
|
@@ -1892,6 +1948,7 @@ export class AgentOrchestrator {
|
|
|
1892
1948
|
subagentId: conv.conversationId,
|
|
1893
1949
|
task: conv.subagentMeta.task,
|
|
1894
1950
|
status: "error",
|
|
1951
|
+
taskOutcome: "failed",
|
|
1895
1952
|
error: conv.subagentMeta.error,
|
|
1896
1953
|
timestamp: Date.now(),
|
|
1897
1954
|
};
|
|
@@ -84,6 +84,12 @@ export interface RunConversationTurnOpts {
|
|
|
84
84
|
* built with an OTLP exporter attached.
|
|
85
85
|
*/
|
|
86
86
|
suppressTelemetry?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Forwarded to `RunInput.maxSteps`: per-run step-ceiling override. Lets the
|
|
89
|
+
* foreground chat turn run a higher ceiling than background/job turns that
|
|
90
|
+
* share the same agent definition.
|
|
91
|
+
*/
|
|
92
|
+
maxSteps?: number;
|
|
87
93
|
/**
|
|
88
94
|
* Forwarded to `RunInput.telemetryAttributes` — extra attributes for the
|
|
89
95
|
* `invoke_agent` root span (e.g. run kind / job name for segmentation).
|
|
@@ -252,6 +258,7 @@ export const runConversationTurn = async (
|
|
|
252
258
|
disablePromptCache: opts.disablePromptCache,
|
|
253
259
|
suppressTelemetry: opts.suppressTelemetry,
|
|
254
260
|
model: opts.model,
|
|
261
|
+
maxSteps: opts.maxSteps,
|
|
255
262
|
volatileContext: opts.volatileContext,
|
|
256
263
|
telemetryAttributes: opts.telemetryAttributes,
|
|
257
264
|
},
|
package/src/state.ts
CHANGED
|
@@ -17,10 +17,23 @@ export interface StateStore {
|
|
|
17
17
|
delete(runId: string): Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Whether a subagent actually accomplished its task — distinct from the *run*
|
|
22
|
+
* status (`status` below), which is "completed" whenever the run merely
|
|
23
|
+
* finished. A subagent that ran to completion but could not do the task (e.g.
|
|
24
|
+
* lacked the required tools) is `runStatus:"completed"` but `taskOutcome:
|
|
25
|
+
* "failed"`. Derived at subagent finalize; "unknown" when it can't be
|
|
26
|
+
* determined. Consumed by the compaction subagent ledger so a failed task can
|
|
27
|
+
* never be summarized as "completed".
|
|
28
|
+
*/
|
|
29
|
+
export type SubagentTaskOutcome = "succeeded" | "failed" | "partial" | "unknown";
|
|
30
|
+
|
|
20
31
|
export interface PendingSubagentResult {
|
|
21
32
|
subagentId: string;
|
|
22
33
|
task: string;
|
|
23
34
|
status: "completed" | "error" | "stopped";
|
|
35
|
+
/** Did the subagent accomplish its task? See {@link SubagentTaskOutcome}. */
|
|
36
|
+
taskOutcome: SubagentTaskOutcome;
|
|
24
37
|
result?: import("@poncho-ai/sdk").RunResult;
|
|
25
38
|
error?: import("@poncho-ai/sdk").AgentFailure;
|
|
26
39
|
timestamp: number;
|
package/test/compaction.test.ts
CHANGED
|
@@ -5,8 +5,10 @@ import type { Message } from "@poncho-ai/sdk";
|
|
|
5
5
|
import {
|
|
6
6
|
compactMessages,
|
|
7
7
|
findSafeSplitPoint,
|
|
8
|
+
findSafeSplitPointByTurns,
|
|
8
9
|
resolveCompactionConfig,
|
|
9
10
|
} from "../src/compaction.js";
|
|
11
|
+
import { deriveTaskOutcome, stripOutcomeVerdict } from "../src/orchestrator/orchestrator.js";
|
|
10
12
|
|
|
11
13
|
// ── Fake model ──────────────────────────────────────────────────────────
|
|
12
14
|
// A MockLanguageModelV3 whose doGenerate returns a fixed text and records the
|
|
@@ -254,9 +256,9 @@ describe("compactMessages", () => {
|
|
|
254
256
|
expect(sentPrompt).toContain("MERGE AND UPDATE");
|
|
255
257
|
});
|
|
256
258
|
|
|
257
|
-
it("still truncates non-prior-summary long messages to
|
|
259
|
+
it("still truncates non-prior-summary long messages to the per-message cap", async () => {
|
|
258
260
|
const { model, prompts } = fakeModel("S");
|
|
259
|
-
const longUser = "X".repeat(
|
|
261
|
+
const longUser = "X".repeat(6000); // > SUMMARIZATION_MESSAGE_TRUNCATION_CHARS (4000)
|
|
260
262
|
const messages: Message[] = [
|
|
261
263
|
userMsg(longUser),
|
|
262
264
|
assistantText("a0"),
|
|
@@ -272,3 +274,123 @@ describe("compactMessages", () => {
|
|
|
272
274
|
expect(sentPrompt).not.toContain("MERGE AND UPDATE");
|
|
273
275
|
});
|
|
274
276
|
});
|
|
277
|
+
|
|
278
|
+
describe("findSafeSplitPointByTurns", () => {
|
|
279
|
+
const turns = (n: number, assistantText = "a"): Message[] => {
|
|
280
|
+
const msgs: Message[] = [];
|
|
281
|
+
for (let i = 0; i < n; i++) {
|
|
282
|
+
msgs.push(userMsg(`u${i}`));
|
|
283
|
+
msgs.push({ role: "assistant", content: `${assistantText}${i}` });
|
|
284
|
+
}
|
|
285
|
+
return msgs;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
it("preserves the last N whole turns verbatim", () => {
|
|
289
|
+
const messages = turns(6); // userIdx = [0,2,4,6,8,10]
|
|
290
|
+
// keepRecentTurns=4, generous token budget → split before the 4th-from-last
|
|
291
|
+
// user message (index 4), preserving turns u2..u5.
|
|
292
|
+
const idx = findSafeSplitPointByTurns(messages, 4, 6, 1_000_000);
|
|
293
|
+
expect(idx).toBe(4);
|
|
294
|
+
const preserved = messages.slice(idx);
|
|
295
|
+
const userCount = preserved.filter((m) => m.role === "user").length;
|
|
296
|
+
expect(userCount).toBe(4);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("reduces N when N turns exceed the preserved-token budget", () => {
|
|
300
|
+
// Each assistant turn is large (~460 tokens); 4 turns preserved would blow a
|
|
301
|
+
// small budget, so it must fall back to fewer turns.
|
|
302
|
+
const big = "Z".repeat(1600);
|
|
303
|
+
const messages = turns(6, big); // userIdx=[0,2,4,6,8,10]
|
|
304
|
+
const idx = findSafeSplitPointByTurns(messages, 4, 6, 1200);
|
|
305
|
+
// n=4 (~1840 tok) and n=3 (~1380 tok) exceed 1200; n=2 (~920 tok) fits →
|
|
306
|
+
// split at userIdx[len-2] = index 8, preserving 2 turns.
|
|
307
|
+
expect(idx).toBe(8);
|
|
308
|
+
expect(messages.slice(idx).filter((m) => m.role === "user").length).toBe(2);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("returns -1 for a single giant turn with no earlier user boundary", () => {
|
|
312
|
+
// One user message followed by many tool rounds — no safe boundary to
|
|
313
|
+
// compact against. (The message-based fallback also finds none.)
|
|
314
|
+
const messages: Message[] = [userMsg("do a big thing")];
|
|
315
|
+
for (let i = 0; i < 8; i++) {
|
|
316
|
+
messages.push(assistantToolCall(`step ${i}`, "run_code"));
|
|
317
|
+
messages.push(toolResult(`out ${i}`));
|
|
318
|
+
}
|
|
319
|
+
expect(findSafeSplitPointByTurns(messages, 4, 6, 1_000_000)).toBe(-1);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe("deriveTaskOutcome / stripOutcomeVerdict", () => {
|
|
324
|
+
it("parses the self-declared verdict", () => {
|
|
325
|
+
expect(deriveTaskOutcome("did it [[OUTCOME: succeeded]] all good", false)).toBe("succeeded");
|
|
326
|
+
expect(deriveTaskOutcome("partial [[OUTCOME: partial]] some left", false)).toBe("partial");
|
|
327
|
+
expect(deriveTaskOutcome("couldn't [[OUTCOME: failed]] no tools", false)).toBe("failed");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("defaults to unknown when no verdict is present (never assumes success)", () => {
|
|
331
|
+
expect(deriveTaskOutcome("I finished everything.", false)).toBe("unknown");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("treats abnormal ends and empty output as failed", () => {
|
|
335
|
+
expect(deriveTaskOutcome("whatever", true)).toBe("failed");
|
|
336
|
+
expect(deriveTaskOutcome(" ", false)).toBe("failed");
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("strips the verdict marker from delivered text", () => {
|
|
340
|
+
expect(stripOutcomeVerdict("Here is the report.\n[[OUTCOME: failed]] missing API key")).toBe(
|
|
341
|
+
"Here is the report.",
|
|
342
|
+
);
|
|
343
|
+
expect(stripOutcomeVerdict("No verdict here")).toBe("No verdict here");
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe("compactMessages — failure fidelity", () => {
|
|
348
|
+
const config = resolveCompactionConfig({ keepRecentTurns: 1 });
|
|
349
|
+
|
|
350
|
+
it("renders a failed subagent as failed (never 'completed') and keeps its reason", async () => {
|
|
351
|
+
const { model } = fakeModel("SUMMARY");
|
|
352
|
+
const messages: Message[] = [
|
|
353
|
+
userMsg("please read my chats"),
|
|
354
|
+
assistantText("delegating to a subagent"),
|
|
355
|
+
userMsg(
|
|
356
|
+
'[Subagent Result] Subagent "read chats" (sub_fail) failed:\n\nI could not access the LinkedIn tools, so I read nothing.',
|
|
357
|
+
{
|
|
358
|
+
_subagentCallback: true,
|
|
359
|
+
subagentId: "sub_fail",
|
|
360
|
+
task: "read chats",
|
|
361
|
+
taskOutcome: "failed",
|
|
362
|
+
} as Message["metadata"],
|
|
363
|
+
),
|
|
364
|
+
assistantText("continuing"),
|
|
365
|
+
userMsg("how did it go?"),
|
|
366
|
+
assistantText("let me check"),
|
|
367
|
+
];
|
|
368
|
+
const res = await compactMessages(model, messages, config);
|
|
369
|
+
const content = res.messages[0]!.content as string;
|
|
370
|
+
expect(content).toContain("## Subagents");
|
|
371
|
+
expect(content).toContain("sub_fail");
|
|
372
|
+
// Rendered with the failed outcome, not "completed".
|
|
373
|
+
expect(content).toContain("— failed");
|
|
374
|
+
expect(content).not.toContain("(sub_fail) — completed");
|
|
375
|
+
// The failure reason survives verbatim.
|
|
376
|
+
expect(content).toContain("could not access the LinkedIn tools");
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("keeps failure-bearing messages when the input budget forces drops", async () => {
|
|
380
|
+
const { model, prompts } = fakeModel("S");
|
|
381
|
+
// Many bulky non-failure messages plus one early failure. The failure must
|
|
382
|
+
// survive into the summarizer input even if older bulk is dropped.
|
|
383
|
+
const messages: Message[] = [userMsg("start")];
|
|
384
|
+
messages.push(toolResult("ERROR: the deploy failed because the token expired"));
|
|
385
|
+
for (let i = 0; i < 400; i++) {
|
|
386
|
+
messages.push(assistantText("filler ".repeat(200) + i));
|
|
387
|
+
messages.push(userMsg(`ok ${i}`));
|
|
388
|
+
}
|
|
389
|
+
messages.push(userMsg("final question"));
|
|
390
|
+
messages.push(assistantText("final answer"));
|
|
391
|
+
await compactMessages(model, messages, config);
|
|
392
|
+
const sentPrompt = prompts.join("\n");
|
|
393
|
+
// The failure line is marked important and never dropped.
|
|
394
|
+
expect(sentPrompt).toContain("the deploy failed because the token expired");
|
|
395
|
+
});
|
|
396
|
+
});
|