@poncho-ai/harness 0.59.18 → 0.60.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.59.18 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.60.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
@@ -8,9 +8,9 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 572.34 KB
12
11
  ESM dist/isolate-F2PPSUL6.js 53.82 KB
13
- ESM ⚡️ Build success in 214ms
12
+ ESM dist/index.js 574.06 KB
13
+ ESM ⚡️ Build success in 251ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 7534ms
16
- DTS dist/index.d.ts 104.01 KB
15
+ DTS ⚡️ Build success in 7890ms
16
+ DTS dist/index.d.ts 105.18 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.60.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`8a5a367`](https://github.com/cesr/poncho-ai/commit/8a5a367dd4b2ea477b3e0146a7253cedcdc34a1c) Thanks [@cesr](https://github.com/cesr)! - Prompt-cache efficiency: tail breakpoint pin + volatile context slot + span attribution.
8
+ - **Second message-history cache breakpoint.** When prior-run tool results
9
+ are still untruncated, the history breakpoint used to sit only at the
10
+ last _stable_ index (before those results) — so a tool-heavy turn
11
+ re-sent everything after it raw on every step. Now a second breakpoint
12
+ is pinned at the true tail, so the current run reads its own growing
13
+ history at 0.1× while the stable entry keeps serving across runs.
14
+ `addPromptCacheBreakpoints` accepts `number | number[]` (out-of-range
15
+ indices dropped, duplicates collapsed). The Anthropic breakpoint budget
16
+ of 4 is now fully spent: static (1h), memory (1h), stable-history, tail.
17
+ - **`RunInput.volatileContext`** — per-run context rendered into the
18
+ _uncached_ dynamic tail of the system prompt. Embedders that previously
19
+ appended volatile blocks (live VFS tree, connected integrations) to the
20
+ agent body — busting the 1h static cache block on every change — can
21
+ pass them here instead, keeping the static block byte-stable (and
22
+ shareable across users when the agent definition is identical). The
23
+ value is captured per conversation so orchestrator-initiated turns
24
+ (continuations, subagent-callback resumes) reuse it. Forwarded through
25
+ `runConversationTurn` opts.
26
+ - **`RunInput.telemetryAttributes`** — extra attributes stamped on the
27
+ `invoke_agent` root span (e.g. `{"poncho.run.kind": "job"}`), letting
28
+ observability backends segment traffic classes without timing
29
+ forensics. Forwarded through `runConversationTurn` opts.
30
+
31
+ ### Patch Changes
32
+
33
+ - Updated dependencies [[`8a5a367`](https://github.com/cesr/poncho-ai/commit/8a5a367dd4b2ea477b3e0146a7253cedcdc34a1c)]:
34
+ - @poncho-ai/sdk@1.16.0
35
+
36
+ ## 0.59.19
37
+
38
+ ### Patch Changes
39
+
40
+ - [#186](https://github.com/cesr/poncho-ai/pull/186) [`b12ccbf`](https://github.com/cesr/poncho-ai/commit/b12ccbf1de63a5ef996b0896898c2afe7238a271) Thanks [@cesr](https://github.com/cesr)! - Stamp the tool-call id onto completed-tool activity lines (`{tcid:<id>}`
41
+ appended after any human detail). Display clients can now join a tool pill to
42
+ its full input/output by id instead of by tool-name + position. The old
43
+ positional/name match misaligns whenever parallel tool calls in a turn
44
+ complete out of declaration order, and can never reach a subagent's
45
+ inner-tool results; id-joining fixes both. The token sits after the first
46
+ `(...)` detail group, so existing clients that only parse inside it are
47
+ unaffected, and it is stripped from model-visible interruption text via the
48
+ new `stripPillMetaTokens` export.
49
+
3
50
  ## 0.59.18
4
51
 
5
52
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -1482,6 +1482,12 @@ declare class AgentHarness {
1482
1482
  private mcpBridge?;
1483
1483
  private subagentManager?;
1484
1484
  private readonly archivedToolResultsByConversation;
1485
+ /** Last explicit `RunInput.volatileContext` per conversation, reused by
1486
+ * orchestrator-initiated turns (continuations, subagent-callback
1487
+ * resumes) that build their own RunInput and can't know the embedder's
1488
+ * volatile blocks. Values are small (the field's contract) and entries
1489
+ * live for the harness instance's lifetime. */
1490
+ private readonly volatileContextByConversation;
1485
1491
  /** Unified storage engine (replaces individual KV-backed stores). */
1486
1492
  storageEngine?: StorageEngine;
1487
1493
  /** Bash environment manager (creates per-tenant bash instances). */
@@ -2135,6 +2141,7 @@ declare const flushTurnDraft: (draft: TurnDraftState) => void;
2135
2141
  declare const buildToolCompletedText: (event: AgentEvent & {
2136
2142
  type: "tool:completed";
2137
2143
  }) => string;
2144
+ declare const stripPillMetaTokens: (line: string) => string;
2138
2145
  declare const recordStandardTurnEvent: (draft: TurnDraftState, event: AgentEvent) => void;
2139
2146
  declare const buildAssistantMetadata: (draft: TurnDraftState, sectionsOverride?: TurnSection[], opts?: {
2140
2147
  id?: string;
@@ -2370,17 +2377,31 @@ interface RunConversationTurnOpts {
2370
2377
  abortSignal?: AbortSignal;
2371
2378
  tenantId?: string | null;
2372
2379
  /**
2373
- * Forwarded to `RunInput.disablePromptCache`. Set true for one-shot
2374
- * turns with no follow-up coming (cron-fired jobs, etc.) so the
2375
- * harness skips the Anthropic cache write.
2380
+ * Forwarded to `RunInput.disablePromptCache`. Only worth it for runs
2381
+ * that are both single-step and one-shot see the RunInput doc; a
2382
+ * multi-step run reads its own growing history through the tail
2383
+ * breakpoint, so disabling it costs more than the write it saves.
2376
2384
  */
2377
2385
  disablePromptCache?: boolean;
2386
+ /**
2387
+ * Forwarded to `RunInput.volatileContext`: per-run context appended to
2388
+ * the uncached dynamic tail of the system prompt. Keep it small — it is
2389
+ * re-sent raw every step. Also captured per conversation so
2390
+ * orchestrator-initiated turns (subagent-callback resumes) reuse the
2391
+ * value from the turn that spawned the subagent.
2392
+ */
2393
+ volatileContext?: string;
2378
2394
  /**
2379
2395
  * Forwarded to `RunInput.suppressTelemetry`. Set true to emit no telemetry
2380
2396
  * for this run (e.g. an incognito / telemetry-off turn) even on a harness
2381
2397
  * built with an OTLP exporter attached.
2382
2398
  */
2383
2399
  suppressTelemetry?: boolean;
2400
+ /**
2401
+ * Forwarded to `RunInput.telemetryAttributes` — extra attributes for the
2402
+ * `invoke_agent` root span (e.g. run kind / job name for segmentation).
2403
+ */
2404
+ telemetryAttributes?: Record<string, string>;
2384
2405
  /**
2385
2406
  * Forwarded to `RunInput.model`. Per-run model override, captured once at
2386
2407
  * run start — safe under concurrent runs on a shared harness, unlike
@@ -2418,4 +2439,4 @@ type NewEntryNoId = NewConversationEntry extends infer T ? T extends NewConversa
2418
2439
  */
2419
2440
  declare const appendEntriesSafe: (store: ConversationStore, conversation: Pick<Conversation, "conversationId" | "ownerId" | "tenantId">, entries: NewEntryNoId[], log: Logger) => Promise<ConversationEntry[]>;
2420
2441
 
2421
- 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, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
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 };
package/dist/index.js CHANGED
@@ -7978,19 +7978,22 @@ function isAnthropicModel(model) {
7978
7978
  }
7979
7979
  return model.provider === "anthropic" || model.provider.includes("anthropic") || model.modelId.includes("anthropic") || model.modelId.includes("claude");
7980
7980
  }
7981
- function addPromptCacheBreakpoints(messages, model, targetIndex) {
7981
+ function addPromptCacheBreakpoints(messages, model, target) {
7982
7982
  if (messages.length === 0 || !isAnthropicModel(model)) {
7983
7983
  return messages;
7984
7984
  }
7985
- const index = targetIndex ?? messages.length - 1;
7986
- if (index < 0 || index >= messages.length) {
7985
+ const requested = target === void 0 ? [messages.length - 1] : Array.isArray(target) ? target : [target];
7986
+ const indices = new Set(
7987
+ requested.filter((i) => i >= 0 && i < messages.length)
7988
+ );
7989
+ if (indices.size === 0) {
7987
7990
  return messages;
7988
7991
  }
7989
7992
  const cacheDirective = {
7990
7993
  anthropic: { cacheControl: { type: "ephemeral" } }
7991
7994
  };
7992
7995
  return messages.map((message, i) => {
7993
- if (i === index) {
7996
+ if (indices.has(i)) {
7994
7997
  return {
7995
7998
  ...message,
7996
7999
  providerOptions: {
@@ -9674,6 +9677,12 @@ var AgentHarness = class _AgentHarness {
9674
9677
  mcpBridge;
9675
9678
  subagentManager;
9676
9679
  archivedToolResultsByConversation = /* @__PURE__ */ new Map();
9680
+ /** Last explicit `RunInput.volatileContext` per conversation, reused by
9681
+ * orchestrator-initiated turns (continuations, subagent-callback
9682
+ * resumes) that build their own RunInput and can't know the embedder's
9683
+ * volatile blocks. Values are small (the field's contract) and entries
9684
+ * live for the harness instance's lifetime. */
9685
+ volatileContextByConversation = /* @__PURE__ */ new Map();
9677
9686
  /** Unified storage engine (replaces individual KV-backed stores). */
9678
9687
  storageEngine;
9679
9688
  /** Bash environment manager (creates per-tenant bash instances). */
@@ -10732,7 +10741,10 @@ var AgentHarness = class _AgentHarness {
10732
10741
  "session.id": input.conversationId
10733
10742
  } : {},
10734
10743
  ...this.telemetryUserId ? { "user.id": this.telemetryUserId } : {},
10735
- ...input.tenantId ? { "tenant.id": input.tenantId } : {}
10744
+ ...input.tenantId ? { "tenant.id": input.tenantId } : {},
10745
+ // Embedder-supplied attribution (e.g. run kind, job name) so
10746
+ // observability backends can segment traffic classes directly.
10747
+ ...input.telemetryAttributes ?? {}
10736
10748
  }
10737
10749
  });
10738
10750
  let spanContext = trace.setSpan(otelContext.active(), rootSpan);
@@ -10816,6 +10828,14 @@ var AgentHarness = class _AgentHarness {
10816
10828
  }
10817
10829
  const hasFullToolResults = hasUntruncatedToolResults(messages);
10818
10830
  const skipTailCache = input.disablePromptCache === true;
10831
+ let volatileRunContext = input.volatileContext;
10832
+ if (input.conversationId) {
10833
+ if (volatileRunContext !== void 0) {
10834
+ this.volatileContextByConversation.set(input.conversationId, volatileRunContext);
10835
+ } else {
10836
+ volatileRunContext = this.volatileContextByConversation.get(input.conversationId);
10837
+ }
10838
+ }
10819
10839
  if (skipTailCache) {
10820
10840
  costLog.debug(`tail cache breakpoint skipped \u2014 disablePromptCache (run=${runId.slice(0, 12)})`);
10821
10841
  } else if (hasFullToolResults) {
@@ -10929,7 +10949,10 @@ ${skillContextWindow}${browserContext}${fsContext}${isolateContext}` : `${agentP
10929
10949
  const timeContext = `
10930
10950
 
10931
10951
  Current UTC time (hour precision): ${hourlyTime}`;
10932
- const dynamicPart = `${todoContext}${timeContext}`;
10952
+ const volatileContext = volatileRunContext ? `
10953
+
10954
+ ${volatileRunContext.trim()}` : "";
10955
+ const dynamicPart = `${todoContext}${volatileContext}${timeContext}`;
10933
10956
  return { staticPart, memoryPart: memoryContext, dynamicPart };
10934
10957
  };
10935
10958
  let { staticPart: staticSystemPart, memoryPart: memorySystemPart, dynamicPart: dynamicSystemPart } = await buildSystemPromptParts();
@@ -11402,7 +11425,7 @@ ${textContent}` };
11402
11425
  const cachedMessages = skipTailCache ? coreMessages : addPromptCacheBreakpoints(
11403
11426
  coreMessages,
11404
11427
  modelInstance,
11405
- hasFullToolResults ? findLastStableCacheIndex(coreMessages) : coreMessages.length - 1
11428
+ hasFullToolResults ? [findLastStableCacheIndex(coreMessages), coreMessages.length - 1] : coreMessages.length - 1
11406
11429
  );
11407
11430
  const useStaticCache = isAnthropicModel(modelInstance);
11408
11431
  const finalMessages = useStaticCache ? [
@@ -11417,7 +11440,10 @@ ${textContent}` };
11417
11440
  // an explicit memory write — its own 1h breakpoint means a
11418
11441
  // memory edit busts THIS block forward but a normal turn reads
11419
11442
  // it (plus everything before it) from cache. Breakpoint budget:
11420
- // Anthropic allows 4; this is #2 of 3 (static, memory, tail).
11443
+ // Anthropic allows 4 and ALL FOUR ARE NOW SPENT (static,
11444
+ // memory, stable-history, tail — the last two collapse to one
11445
+ // when there are no untruncated prior tool results). Adding a
11446
+ // fifth cache_control block anywhere is an API 400.
11421
11447
  ...memorySystemPart.length > 0 ? [{
11422
11448
  role: "system",
11423
11449
  content: memorySystemPart,
@@ -12739,8 +12765,10 @@ var buildToolCompletedText = (event) => {
12739
12765
  if (event.tool === "web_search" && Array.isArray(output?.results)) {
12740
12766
  text += ` \u2014 ${output.results.length} result${output.results.length !== 1 ? "s" : ""}`;
12741
12767
  }
12768
+ if (event.toolCallId) text += ` {tcid:${event.toolCallId}}`;
12742
12769
  return text;
12743
12770
  };
12771
+ var stripPillMetaTokens = (line) => line.replace(/\s*\{tcid:[^}]+\}/g, "");
12744
12772
  var recordStandardTurnEvent = (draft, event) => {
12745
12773
  if (event.type === "model:chunk") {
12746
12774
  if (draft.currentTools.length > 0) {
@@ -13622,7 +13650,7 @@ var AgentOrchestrator = class {
13622
13650
  for (const a of decidedApprovals) {
13623
13651
  if (a.decision === "approved" && a.toolCallId) {
13624
13652
  callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
13625
- const toolText = `- done \`${a.tool}\``;
13653
+ const toolText = `- done \`${a.tool}\`` + (a.toolCallId ? ` {tcid:${a.toolCallId}}` : "");
13626
13654
  draft.toolTimeline.push(toolText);
13627
13655
  draft.currentTools.push(toolText);
13628
13656
  } else if (a.toolCallId) {
@@ -14606,7 +14634,9 @@ var runConversationTurn = async (opts) => {
14606
14634
  abortSignal: opts.abortSignal,
14607
14635
  disablePromptCache: opts.disablePromptCache,
14608
14636
  suppressTelemetry: opts.suppressTelemetry,
14609
- model: opts.model
14637
+ model: opts.model,
14638
+ volatileContext: opts.volatileContext,
14639
+ telemetryAttributes: opts.telemetryAttributes
14610
14640
  },
14611
14641
  initialContextTokens: conversation.contextTokens ?? 0,
14612
14642
  initialContextWindow: conversation.contextWindow ?? 0,
@@ -14783,8 +14813,10 @@ var runConversationTurn = async (opts) => {
14783
14813
  const parts = [];
14784
14814
  if (draft.assistantResponse.length > 0) parts.push(draft.assistantResponse);
14785
14815
  if (draft.toolTimeline.length > 0) {
14786
- parts.push(`Tool activity before interruption:
14787
- ${draft.toolTimeline.join("\n")}`);
14816
+ parts.push(
14817
+ `Tool activity before interruption:
14818
+ ${draft.toolTimeline.map(stripPillMetaTokens).join("\n")}`
14819
+ );
14788
14820
  }
14789
14821
  parts.push(`[This turn was interrupted: ${reason}. The work above may be incomplete.]`);
14790
14822
  return [
@@ -14991,6 +15023,7 @@ export {
14991
15023
  runConversationTurn,
14992
15024
  slugifyStorageComponent,
14993
15025
  startOpenAICodexDeviceAuth,
15026
+ stripPillMetaTokens,
14994
15027
  verifyTenantToken,
14995
15028
  withToolResultArchiveParam,
14996
15029
  writeOpenAICodexSession
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.59.18",
3
+ "version": "0.60.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.15.2"
37
+ "@poncho-ai/sdk": "1.16.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "esbuild": ">=0.17.0",
package/src/harness.ts CHANGED
@@ -937,6 +937,12 @@ export class AgentHarness {
937
937
  private mcpBridge?: LocalMcpBridge;
938
938
  private subagentManager?: SubagentManager;
939
939
  private readonly archivedToolResultsByConversation = new Map<string, Record<string, ArchivedToolResult>>();
940
+ /** Last explicit `RunInput.volatileContext` per conversation, reused by
941
+ * orchestrator-initiated turns (continuations, subagent-callback
942
+ * resumes) that build their own RunInput and can't know the embedder's
943
+ * volatile blocks. Values are small (the field's contract) and entries
944
+ * live for the harness instance's lifetime. */
945
+ private readonly volatileContextByConversation = new Map<string, string>();
940
946
 
941
947
  /** Unified storage engine (replaces individual KV-backed stores). */
942
948
  storageEngine?: StorageEngine;
@@ -2224,6 +2230,9 @@ export class AgentHarness {
2224
2230
  : {}),
2225
2231
  ...(this.telemetryUserId ? { "user.id": this.telemetryUserId } : {}),
2226
2232
  ...(input.tenantId ? { "tenant.id": input.tenantId } : {}),
2233
+ // Embedder-supplied attribution (e.g. run kind, job name) so
2234
+ // observability backends can segment traffic classes directly.
2235
+ ...(input.telemetryAttributes ?? {}),
2227
2236
  },
2228
2237
  });
2229
2238
 
@@ -2337,6 +2346,20 @@ export class AgentHarness {
2337
2346
  // 1-hour static breakpoint on the system prompt is always on — it
2338
2347
  // amortizes across every later turn or job within the hour.
2339
2348
  const skipTailCache = input.disablePromptCache === true;
2349
+ // Effective volatile context for this run. An explicit value (even "")
2350
+ // wins and is remembered per conversation so orchestrator-initiated
2351
+ // turns on the same conversation — continuations, subagent-callback
2352
+ // resumes — reuse the value from the turn that set it instead of
2353
+ // silently dropping it (they build their own RunInput and can't know
2354
+ // the embedder's blocks).
2355
+ let volatileRunContext = input.volatileContext;
2356
+ if (input.conversationId) {
2357
+ if (volatileRunContext !== undefined) {
2358
+ this.volatileContextByConversation.set(input.conversationId, volatileRunContext);
2359
+ } else {
2360
+ volatileRunContext = this.volatileContextByConversation.get(input.conversationId);
2361
+ }
2362
+ }
2340
2363
  if (skipTailCache) {
2341
2364
  costLog.debug(`tail cache breakpoint skipped — disablePromptCache (run=${runId.slice(0, 12)})`);
2342
2365
  } else if (hasFullToolResults) {
@@ -2485,7 +2508,11 @@ Code is wrapped in an async IIFE — use \`return\` to return a value to the too
2485
2508
  return `${weekday} ${d.toISOString().slice(0, 13)}Z`;
2486
2509
  })();
2487
2510
  const timeContext = `\n\nCurrent UTC time (hour precision): ${hourlyTime}`;
2488
- const dynamicPart = `${todoContext}${timeContext}`;
2511
+ // Embedder-supplied volatile blocks (live VFS tree, connected
2512
+ // integrations, …) land here — uncached by design, so their churn
2513
+ // never busts the 1h static/memory cache entries above.
2514
+ const volatileContext = volatileRunContext ? `\n\n${volatileRunContext.trim()}` : "";
2515
+ const dynamicPart = `${todoContext}${volatileContext}${timeContext}`;
2489
2516
  return { staticPart, memoryPart: memoryContext, dynamicPart };
2490
2517
  };
2491
2518
  let { staticPart: staticSystemPart, memoryPart: memorySystemPart, dynamicPart: dynamicSystemPart } =
@@ -3107,19 +3134,24 @@ Code is wrapped in an async IIFE — use \`return\` to return a value to the too
3107
3134
  // below: omitted from the request when undefined.
3108
3135
  const temperature = agent.frontmatter.model?.temperature;
3109
3136
  const maxTokens = agent.frontmatter.model?.maxTokens;
3110
- // Place the tail breakpoint before any untruncated tool-result so
3111
- // we cache only the stable prefix when prior-run tool results are
3112
- // still full-fidelity. Otherwise cache at the history tail. When
3113
- // `skipTailCache` is set (per-run override), don't write the tail
3114
- // breakpoint at all. The 1-hour static-prefix breakpoint is added
3115
- // separately when assembling the final messages array.
3137
+ // Two history breakpoints: one just before any untruncated
3138
+ // prior-run tool-result (the stable prefix keeps its cross-run
3139
+ // cache entry truncateHistoricalToolResults rewrites everything
3140
+ // after it next run) AND one pinned at the true tail so the
3141
+ // current run's own steps read the growing history at 0.1x instead
3142
+ // of re-paying it raw every step. Without the tail pin, a
3143
+ // tool-heavy turn with full prior results re-sent everything past
3144
+ // the stable index uncached on all of its (up to maxSteps) steps.
3145
+ // When there are no untruncated results the two collapse to the
3146
+ // tail (addPromptCacheBreakpoints dedupes). When `skipTailCache`
3147
+ // is set (per-run override), write no history breakpoints at all.
3116
3148
  const cachedMessages = skipTailCache
3117
3149
  ? coreMessages
3118
3150
  : addPromptCacheBreakpoints(
3119
3151
  coreMessages,
3120
3152
  modelInstance,
3121
3153
  hasFullToolResults
3122
- ? findLastStableCacheIndex(coreMessages)
3154
+ ? [findLastStableCacheIndex(coreMessages), coreMessages.length - 1]
3123
3155
  : coreMessages.length - 1,
3124
3156
  );
3125
3157
 
@@ -3146,7 +3178,10 @@ Code is wrapped in an async IIFE — use \`return\` to return a value to the too
3146
3178
  // an explicit memory write — its own 1h breakpoint means a
3147
3179
  // memory edit busts THIS block forward but a normal turn reads
3148
3180
  // it (plus everything before it) from cache. Breakpoint budget:
3149
- // Anthropic allows 4; this is #2 of 3 (static, memory, tail).
3181
+ // Anthropic allows 4 and ALL FOUR ARE NOW SPENT (static,
3182
+ // memory, stable-history, tail — the last two collapse to one
3183
+ // when there are no untruncated prior tool results). Adding a
3184
+ // fifth cache_control block anywhere is an API 400.
3150
3185
  ...(memorySystemPart.length > 0
3151
3186
  ? [{
3152
3187
  role: "system" as const,
@@ -13,6 +13,7 @@ export {
13
13
  cloneSections,
14
14
  flushTurnDraft,
15
15
  buildToolCompletedText,
16
+ stripPillMetaTokens,
16
17
  recordStandardTurnEvent,
17
18
  buildAssistantMetadata,
18
19
  executeConversationTurn,
@@ -904,7 +904,8 @@ export class AgentOrchestrator {
904
904
  for (const a of decidedApprovals) {
905
905
  if (a.decision === "approved" && a.toolCallId) {
906
906
  callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
907
- const toolText = `- done \`${a.tool}\``;
907
+ const toolText =
908
+ `- done \`${a.tool}\`` + (a.toolCallId ? ` {tcid:${a.toolCallId}}` : "");
908
909
  draft.toolTimeline.push(toolText);
909
910
  draft.currentTools.push(toolText);
910
911
  } else if (a.toolCallId) {
@@ -35,6 +35,7 @@ import {
35
35
  createTurnDraftState,
36
36
  executeConversationTurn,
37
37
  flushTurnDraft,
38
+ stripPillMetaTokens,
38
39
  } from "./turn.js";
39
40
 
40
41
  const log = createLogger("orchestrator");
@@ -63,17 +64,31 @@ export interface RunConversationTurnOpts {
63
64
  abortSignal?: AbortSignal;
64
65
  tenantId?: string | null;
65
66
  /**
66
- * Forwarded to `RunInput.disablePromptCache`. Set true for one-shot
67
- * turns with no follow-up coming (cron-fired jobs, etc.) so the
68
- * harness skips the Anthropic cache write.
67
+ * Forwarded to `RunInput.disablePromptCache`. Only worth it for runs
68
+ * that are both single-step and one-shot see the RunInput doc; a
69
+ * multi-step run reads its own growing history through the tail
70
+ * breakpoint, so disabling it costs more than the write it saves.
69
71
  */
70
72
  disablePromptCache?: boolean;
73
+ /**
74
+ * Forwarded to `RunInput.volatileContext`: per-run context appended to
75
+ * the uncached dynamic tail of the system prompt. Keep it small — it is
76
+ * re-sent raw every step. Also captured per conversation so
77
+ * orchestrator-initiated turns (subagent-callback resumes) reuse the
78
+ * value from the turn that spawned the subagent.
79
+ */
80
+ volatileContext?: string;
71
81
  /**
72
82
  * Forwarded to `RunInput.suppressTelemetry`. Set true to emit no telemetry
73
83
  * for this run (e.g. an incognito / telemetry-off turn) even on a harness
74
84
  * built with an OTLP exporter attached.
75
85
  */
76
86
  suppressTelemetry?: boolean;
87
+ /**
88
+ * Forwarded to `RunInput.telemetryAttributes` — extra attributes for the
89
+ * `invoke_agent` root span (e.g. run kind / job name for segmentation).
90
+ */
91
+ telemetryAttributes?: Record<string, string>;
77
92
  /**
78
93
  * Forwarded to `RunInput.model`. Per-run model override, captured once at
79
94
  * run start — safe under concurrent runs on a shared harness, unlike
@@ -237,6 +252,8 @@ export const runConversationTurn = async (
237
252
  disablePromptCache: opts.disablePromptCache,
238
253
  suppressTelemetry: opts.suppressTelemetry,
239
254
  model: opts.model,
255
+ volatileContext: opts.volatileContext,
256
+ telemetryAttributes: opts.telemetryAttributes,
240
257
  },
241
258
  initialContextTokens: conversation.contextTokens ?? 0,
242
259
  initialContextWindow: conversation.contextWindow ?? 0,
@@ -435,7 +452,11 @@ export const runConversationTurn = async (
435
452
  const parts: string[] = [];
436
453
  if (draft.assistantResponse.length > 0) parts.push(draft.assistantResponse);
437
454
  if (draft.toolTimeline.length > 0) {
438
- parts.push(`Tool activity before interruption:\n${draft.toolTimeline.join("\n")}`);
455
+ parts.push(
456
+ `Tool activity before interruption:\n${draft.toolTimeline
457
+ .map(stripPillMetaTokens)
458
+ .join("\n")}`,
459
+ );
439
460
  }
440
461
  parts.push(`[This turn was interrupted: ${reason}. The work above may be incomplete.]`);
441
462
  return [
@@ -120,9 +120,25 @@ export const buildToolCompletedText = (event: AgentEvent & { type: "tool:complet
120
120
  text += ` \u2014 ${(output.results as unknown[]).length} result${(output.results as unknown[]).length !== 1 ? "s" : ""}`;
121
121
  }
122
122
 
123
+ // Trailing machine token: the tool-call id this pill corresponds to. Lets a
124
+ // display client join the pill to its full input/output by id rather than
125
+ // by tool-name+position (which misaligns whenever parallel tool calls in a
126
+ // turn complete out of declaration order, and can't reach a subagent's
127
+ // inner-tool results at all). Appended AFTER any human detail/parens so
128
+ // older clients \u2014 which only read inside the first `(...)` \u2014 ignore it.
129
+ // Stripped from model-visible interruption text via stripPillMetaTokens.
130
+ if (event.toolCallId) text += ` {tcid:${event.toolCallId}}`;
131
+
123
132
  return text;
124
133
  };
125
134
 
135
+ // Remove machine tokens (e.g. `{tcid:\u2026}`) that buildToolCompletedText appends
136
+ // to activity lines for the display client. Use anywhere a tool-timeline line
137
+ // is folded into MODEL-visible text (e.g. interruption reconstruction) so the
138
+ // internal id never leaks into the prompt.
139
+ export const stripPillMetaTokens = (line: string): string =>
140
+ line.replace(/\s*\{tcid:[^}]+\}/g, "");
141
+
126
142
  export const recordStandardTurnEvent = (draft: TurnDraftState, event: AgentEvent): void => {
127
143
  if (event.type === "model:chunk") {
128
144
  if (draft.currentTools.length > 0) {
@@ -17,23 +17,32 @@ export function isAnthropicModel(model: LanguageModel): boolean {
17
17
  * explicit opt-in (Anthropic). For providers with automatic caching
18
18
  * (OpenAI), messages are returned unchanged.
19
19
  *
20
- * For Anthropic, marks the target message with ephemeral cache control so
21
- * the conversation prefix is incrementally cached across steps. When
22
- * `targetIndex` is omitted, the last message is used (default behavior).
23
- * Callers that want to cache only a stable prefix (e.g. skipping tool
24
- * results that will be truncated next turn) can pass an earlier index.
20
+ * For Anthropic, marks the target message(s) with ephemeral cache control
21
+ * so the conversation prefix is incrementally cached across steps. When
22
+ * `target` is omitted, the last message is used (default behavior).
23
+ * Callers can pass an earlier index to cache only a stable prefix (e.g.
24
+ * skipping tool results that will be truncated next turn), or an array to
25
+ * mark several — typically `[stableIndex, tail]` so the stable prefix
26
+ * keeps its cross-run entry while the moving tail serves within-run reads.
27
+ * Out-of-range indices are dropped and duplicates collapse to one mark
28
+ * (a message must never carry two cache_control blocks).
25
29
  */
26
30
  export function addPromptCacheBreakpoints(
27
31
  messages: ModelMessage[],
28
32
  model: LanguageModel,
29
- targetIndex?: number,
33
+ target?: number | number[],
30
34
  ): ModelMessage[] {
31
35
  if (messages.length === 0 || !isAnthropicModel(model)) {
32
36
  return messages;
33
37
  }
34
38
 
35
- const index = targetIndex ?? messages.length - 1;
36
- if (index < 0 || index >= messages.length) {
39
+ const requested = target === undefined
40
+ ? [messages.length - 1]
41
+ : Array.isArray(target) ? target : [target];
42
+ const indices = new Set(
43
+ requested.filter((i) => i >= 0 && i < messages.length),
44
+ );
45
+ if (indices.size === 0) {
37
46
  return messages;
38
47
  }
39
48
 
@@ -42,7 +51,7 @@ export function addPromptCacheBreakpoints(
42
51
  };
43
52
 
44
53
  return messages.map((message, i) => {
45
- if (i === index) {
54
+ if (indices.has(i)) {
46
55
  return {
47
56
  ...message,
48
57
  providerOptions: {
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { AgentEvent } from "@poncho-ai/sdk";
3
+ import { buildToolCompletedText, stripPillMetaTokens } from "../src/orchestrator/turn.js";
4
+
5
+ const completed = (over: Partial<AgentEvent & { type: "tool:completed" }>) =>
6
+ buildToolCompletedText({
7
+ type: "tool:completed",
8
+ tool: "bash",
9
+ toolCallId: "toolu_abc123",
10
+ input: { command: "ls -la" },
11
+ output: {},
12
+ duration: 45,
13
+ outputTokenEstimate: 0,
14
+ ...over,
15
+ } as AgentEvent & { type: "tool:completed" });
16
+
17
+ describe("buildToolCompletedText tcid token", () => {
18
+ it("appends the tool-call id after the human detail", () => {
19
+ const line = completed({});
20
+ expect(line).toContain("- done `bash`");
21
+ expect(line).toContain("(45ms, ls -la)");
22
+ expect(line.endsWith("{tcid:toolu_abc123}")).toBe(true);
23
+ // Token sits AFTER the first (...) detail group so old clients ignore it.
24
+ expect(line.indexOf("{tcid:")).toBeGreaterThan(line.indexOf(")"));
25
+ });
26
+
27
+ it("omits the token when there is no tool-call id", () => {
28
+ const line = completed({ toolCallId: undefined as unknown as string });
29
+ expect(line).not.toContain("{tcid:");
30
+ });
31
+
32
+ it("keeps the id intact alongside the subagent token", () => {
33
+ const line = completed({
34
+ tool: "spawn_subagent",
35
+ toolCallId: "toolu_xyz",
36
+ input: { task: "research" },
37
+ output: { subagentId: "conv_child" },
38
+ });
39
+ expect(line).toContain("[subagent:conv_child]");
40
+ expect(line).toContain("{tcid:toolu_xyz}");
41
+ });
42
+ });
43
+
44
+ describe("stripPillMetaTokens", () => {
45
+ it("removes the tcid token (and its leading space) for model-visible text", () => {
46
+ expect(stripPillMetaTokens("- done `bash` (45ms, ls) {tcid:toolu_abc}")).toBe(
47
+ "- done `bash` (45ms, ls)",
48
+ );
49
+ });
50
+
51
+ it("is a no-op for lines without a token", () => {
52
+ expect(stripPillMetaTokens("- start `web_search` (\"q\")")).toBe(
53
+ "- start `web_search` (\"q\")",
54
+ );
55
+ });
56
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { ModelMessage } from "ai";
3
+ import { addPromptCacheBreakpoints } from "../src/prompt-cache.js";
4
+
5
+ const ANTHROPIC_MODEL = "anthropic/claude-opus-4-8";
6
+ const OPENAI_MODEL = "openai/gpt-4o";
7
+
8
+ function messages(n: number): ModelMessage[] {
9
+ return Array.from({ length: n }, (_, i) => ({
10
+ role: i % 2 === 0 ? ("user" as const) : ("assistant" as const),
11
+ content: `message ${i}`,
12
+ }));
13
+ }
14
+
15
+ function markedIndices(result: ModelMessage[]): number[] {
16
+ return result
17
+ .map((m, i) => ((m.providerOptions as Record<string, unknown> | undefined)?.anthropic ? i : -1))
18
+ .filter((i) => i >= 0);
19
+ }
20
+
21
+ describe("addPromptCacheBreakpoints", () => {
22
+ it("marks the last message by default", () => {
23
+ const result = addPromptCacheBreakpoints(messages(4), ANTHROPIC_MODEL);
24
+ expect(markedIndices(result)).toEqual([3]);
25
+ });
26
+
27
+ it("marks a single explicit index", () => {
28
+ const result = addPromptCacheBreakpoints(messages(4), ANTHROPIC_MODEL, 1);
29
+ expect(markedIndices(result)).toEqual([1]);
30
+ });
31
+
32
+ it("marks stable + tail when given an array", () => {
33
+ const result = addPromptCacheBreakpoints(messages(6), ANTHROPIC_MODEL, [2, 5]);
34
+ expect(markedIndices(result)).toEqual([2, 5]);
35
+ });
36
+
37
+ it("collapses duplicate indices to one mark (stable == tail)", () => {
38
+ const result = addPromptCacheBreakpoints(messages(4), ANTHROPIC_MODEL, [3, 3]);
39
+ expect(markedIndices(result)).toEqual([3]);
40
+ const marked = result[3].providerOptions as Record<string, unknown>;
41
+ // One cacheControl object, not a doubled/merged mess.
42
+ expect(marked.anthropic).toEqual({ cacheControl: { type: "ephemeral" } });
43
+ });
44
+
45
+ it("drops out-of-range indices (findLastStableCacheIndex can return -1)", () => {
46
+ const result = addPromptCacheBreakpoints(messages(4), ANTHROPIC_MODEL, [-1, 3]);
47
+ expect(markedIndices(result)).toEqual([3]);
48
+ });
49
+
50
+ it("returns messages unchanged when every index is out of range", () => {
51
+ const input = messages(2);
52
+ const result = addPromptCacheBreakpoints(input, ANTHROPIC_MODEL, [-1, 9]);
53
+ expect(result).toBe(input);
54
+ });
55
+
56
+ it("leaves non-Anthropic models untouched", () => {
57
+ const input = messages(3);
58
+ const result = addPromptCacheBreakpoints(input, OPENAI_MODEL, [0, 2]);
59
+ expect(result).toBe(input);
60
+ });
61
+
62
+ it("preserves existing providerOptions on the marked message", () => {
63
+ const input = messages(2);
64
+ input[1] = { ...input[1], providerOptions: { other: { keep: true } } as never };
65
+ const result = addPromptCacheBreakpoints(input, ANTHROPIC_MODEL, [1]);
66
+ const opts = result[1].providerOptions as Record<string, unknown>;
67
+ expect(opts.other).toEqual({ keep: true });
68
+ expect(opts.anthropic).toEqual({ cacheControl: { type: "ephemeral" } });
69
+ });
70
+ });