@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.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +47 -0
- package/dist/index.d.ts +25 -4
- package/dist/index.js +45 -12
- package/package.json +2 -2
- package/src/harness.ts +44 -9
- package/src/orchestrator/index.ts +1 -0
- package/src/orchestrator/orchestrator.ts +2 -1
- package/src/orchestrator/run-conversation-turn.ts +25 -4
- package/src/orchestrator/turn.ts +16 -0
- package/src/prompt-cache.ts +18 -9
- package/test/pill-tool-call-id.test.ts +56 -0
- package/test/prompt-cache.test.ts +70 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
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
|
[34mCLI[39m tsup v8.5.1
|
|
9
9
|
[34mCLI[39m Target: es2022
|
|
10
10
|
[34mESM[39m Build start
|
|
11
|
-
[32mESM[39m [1mdist/index.js [22m[32m572.34 KB[39m
|
|
12
11
|
[32mESM[39m [1mdist/isolate-F2PPSUL6.js [22m[32m53.82 KB[39m
|
|
13
|
-
[32mESM[39m
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m574.06 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 251ms
|
|
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 7890ms
|
|
16
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m105.18 KB[39m
|
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`.
|
|
2374
|
-
*
|
|
2375
|
-
*
|
|
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,
|
|
7981
|
+
function addPromptCacheBreakpoints(messages, model, target) {
|
|
7982
7982
|
if (messages.length === 0 || !isAnthropicModel(model)) {
|
|
7983
7983
|
return messages;
|
|
7984
7984
|
}
|
|
7985
|
-
const
|
|
7986
|
-
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
14787
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
3111
|
-
//
|
|
3112
|
-
//
|
|
3113
|
-
//
|
|
3114
|
-
//
|
|
3115
|
-
//
|
|
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
|
|
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,
|
|
@@ -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 =
|
|
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`.
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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(
|
|
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 [
|
package/src/orchestrator/turn.ts
CHANGED
|
@@ -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) {
|
package/src/prompt-cache.ts
CHANGED
|
@@ -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
|
|
21
|
-
* the conversation prefix is incrementally cached across steps. When
|
|
22
|
-
* `
|
|
23
|
-
* Callers
|
|
24
|
-
* results that will be truncated next turn)
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
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
|
|
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
|
+
});
|