@poncho-ai/harness 0.61.0 → 0.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +14 -0
- package/dist/index.d.ts +35 -1
- package/dist/index.js +78 -39
- package/package.json +1 -1
- package/src/orchestrator/index.ts +3 -0
- package/src/orchestrator/orchestrator.ts +11 -37
- package/src/orchestrator/turn.ts +130 -0
- package/test/resume-transcript-integrity.test.ts +156 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.61.
|
|
2
|
+
> @poncho-ai/harness@0.61.1 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[32m580.50 KB[39m
|
|
11
12
|
[32mESM[39m [1mdist/isolate-F2PPSUL6.js [22m[32m53.82 KB[39m
|
|
12
|
-
[32mESM[39m
|
|
13
|
-
[32mESM[39m ⚡️ Build success in 267ms
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 227ms
|
|
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 7778ms
|
|
16
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m110.56 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.61.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`a16f4fa`](https://github.com/cesr/poncho-ai/commit/a16f4fa32f0809d6ff73f0041cb0e0a3fb980bb1) Thanks [@cesr](https://github.com/cesr)! - Fix: checkpoint-resume no longer drops a turn's user message from the model
|
|
8
|
+
transcript. Extract the checkpoint→transcript assembly the orchestrator's
|
|
9
|
+
`resumeRunFromCheckpoint` used into shared, exported helpers —
|
|
10
|
+
`assembleCheckpointMessages`, `buildToolResultMessage`, `buildResumeCheckpoints`
|
|
11
|
+
— so external embedders (which execute gated tools themselves) reconstruct the
|
|
12
|
+
canonical transcript identically instead of re-deriving the index arithmetic
|
|
13
|
+
and drifting. Add a transcript-integrity guard in `applyTurnMetadata` that
|
|
14
|
+
logs when a finalize would leave the latest user message out of the
|
|
15
|
+
model-facing transcript.
|
|
16
|
+
|
|
3
17
|
## 0.61.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -2222,6 +2222,40 @@ declare const buildApprovalCheckpoints: ({ approvals, runId, checkpointMessages,
|
|
|
2222
2222
|
pendingToolCalls: PendingToolCall[];
|
|
2223
2223
|
kind?: "approval" | "device";
|
|
2224
2224
|
}) => NonNullable<Conversation["pendingApprovals"]>;
|
|
2225
|
+
/** Reconstruct the full canonical transcript a checkpoint resumes from: the
|
|
2226
|
+
* prior history (before the checkpointed turn) + the checkpoint delta.
|
|
2227
|
+
* Handles both storage conventions via `normalizeApprovalCheckpoint` — the
|
|
2228
|
+
* initial checkpoint (base = prior length, delta) and a resume-created one
|
|
2229
|
+
* (base 0, full canonical). */
|
|
2230
|
+
declare const assembleCheckpointMessages: (conversation: Conversation, checkpoint: StoredApproval) => Message[];
|
|
2231
|
+
/** Pair an assistant tool-call message's `tool_calls` with externally-computed
|
|
2232
|
+
* results into the single `role:"tool"` message a continuation reads.
|
|
2233
|
+
* Returns undefined when `assistantMsg` isn't an assistant message with
|
|
2234
|
+
* parseable tool calls. Used to persist the resume's canonical history so it
|
|
2235
|
+
* matches what `continueFromToolResult` fed the model. Missing results
|
|
2236
|
+
* default to the deferred-error marker (same text the run uses). */
|
|
2237
|
+
declare const buildToolResultMessage: (assistantMsg: Message | undefined, toolResults: Array<{
|
|
2238
|
+
callId: string;
|
|
2239
|
+
toolName: string;
|
|
2240
|
+
result?: unknown;
|
|
2241
|
+
error?: string;
|
|
2242
|
+
}>) => Message | undefined;
|
|
2243
|
+
/** Build the `pendingApprovals` rows for a checkpoint reached DURING a resume
|
|
2244
|
+
* continuation. Stores the WHOLE canonical history the continuation ran with
|
|
2245
|
+
* (`priorMessages` = prior + tool result + new delta) with
|
|
2246
|
+
* `baseMessageCount: 0`. Keeping the full history here — rather than a delta
|
|
2247
|
+
* + a base count into a different message array — is what lets the next
|
|
2248
|
+
* resume reconstruct with no index arithmetic. */
|
|
2249
|
+
declare const buildResumeCheckpoints: ({ priorMessages, checkpointEvent, runId, kind, }: {
|
|
2250
|
+
priorMessages: Message[];
|
|
2251
|
+
checkpointEvent: {
|
|
2252
|
+
approvals: ApprovalEventItem[];
|
|
2253
|
+
checkpointMessages: Message[];
|
|
2254
|
+
pendingToolCalls: PendingToolCall[];
|
|
2255
|
+
};
|
|
2256
|
+
runId: string;
|
|
2257
|
+
kind?: "approval" | "device";
|
|
2258
|
+
}) => NonNullable<Conversation["pendingApprovals"]>;
|
|
2225
2259
|
declare const applyTurnMetadata: (conv: Conversation, meta: TurnResultMetadata, opts?: {
|
|
2226
2260
|
clearContinuation?: boolean;
|
|
2227
2261
|
clearApprovals?: boolean;
|
|
@@ -2503,4 +2537,4 @@ type NewEntryNoId = NewConversationEntry extends infer T ? T extends NewConversa
|
|
|
2503
2537
|
*/
|
|
2504
2538
|
declare const appendEntriesSafe: (store: ConversationStore, conversation: Pick<Conversation, "conversationId" | "ownerId" | "tenantId">, entries: NewEntryNoId[], log: Logger) => Promise<ConversationEntry[]>;
|
|
2505
2539
|
|
|
2506
|
-
export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CallbackStartedEntry, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationEntry, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TAGLINE, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, type NewConversationEntry, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentResultEntry, type SubagentSpawnResult, type SubagentSummary, type SubagentTaskOutcome, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, abnormalEndResponse, appendEntriesSafe, applyTurnMetadata, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildSkillContextWindow, buildToolCompletedText, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, findSafeSplitPointByTurns, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPendingSubagentResults, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, lastAssistantText, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, realResponseText, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, stripPillMetaTokens, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
|
2540
|
+
export { type ActiveConversationRun, type ActiveSubagentRun, type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, AgentOrchestrator, type ApprovalEventItem, type ArchivedToolResult$1 as ArchivedToolResult, type BashConfig, BashEnvironmentManager, type BashExecutionLimits, type BuiltInToolToggles, CALLBACK_LOCK_STALE_MS, type CallbackStartedEntry, type CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationEntry, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, DEFAULT_TAGLINE, DEFAULT_TEMPERATURE, DEFAULT_TIMEOUT, type DefaultAgentDefinitionOptions, type EventSink, type ExecuteTurnResult, type HarnessOptions, type HarnessRunOutput, type HistorySource, InMemoryConversationStore, InMemoryEngine, InMemoryStateStore, type IsolateBinding, type IsolateConfig, LocalMcpBridge, LocalUploadStore, MAX_CONCURRENT_SUBAGENTS, MAX_CONTINUATION_COUNT, MAX_SUBAGENT_CALLBACK_COUNT, MAX_SUBAGENT_NESTING, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, type MountProvider, type NetworkConfig, type NewConversationEntry, OPENAI_CODEX_CLIENT_ID, type OpenAICodexAuthConfig, type OpenAICodexDeviceAuthRequest, type OpenAICodexSession, type OrchestratorHooks, type OrchestratorOptions, type OtlpConfig, type OtlpOption, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PendingSubagentApproval, type PendingSubagentResult, type PendingToolCall, type PonchoConfig, PonchoFsAdapter, PostgresEngine, type ProviderConfig, type Recurrence, type RecurrenceType, type Reminder, type ReminderCreateInput, type ReminderStatus, type ReminderStore, type RemoteMcpServerConfig, type RunConversationTurnOpts, type RunConversationTurnResult, type RunOutcome, type RunRequest, type RuntimeRenderContext, S3UploadStore, STALE_SUBAGENT_THRESHOLD_MS, STORAGE_SCHEMA_VERSION, type SecretsStore, type SkillContextEntry, type SkillMetadata, type SkillSource, SqliteEngine, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type StorageEngine, type StorageFactoryOptions, type StorageProvider, type StoredApproval, type SubagentManager, type SubagentResult, type SubagentResultEntry, type SubagentSpawnResult, type SubagentSummary, type SubagentTaskOutcome, type SubagentTranscript, type SubagentTranscriptMode, TOOL_RESULT_ARCHIVE_PARAM, type TelemetryConfig, TelemetryEmitter, type TenantTokenPayload, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type TurnDraftState, type TurnResultMetadata, type TurnSection, type UploadStore, type UploadsConfig, VFS_SCHEME, VercelBlobUploadStore, type VfsDirEntry, type VfsStat, type VirtualMount, abnormalEndResponse, appendEntriesSafe, applyTurnMetadata, assembleCheckpointMessages, buildAgentDirectoryName, buildApprovalCheckpoints, buildAssistantMetadata, buildResumeCheckpoints, buildSkillContextWindow, buildToolCompletedText, buildToolResultMessage, cloneSections, compactMessages, completeOpenAICodexDeviceAuth, computeNextOccurrence, createBashTool, createConversationStore, createConversationStoreFromEngine, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createEditTool, createMemoryStore, createMemoryStoreFromEngine, createMemoryTools, createModelProvider, createReminderStore, createReminderStoreFromEngine, createReminderTools, createSearchTools, createSecretsStore, createSkillTools, createStateStore, createStorageEngine, createSubagentTools, createTodoStoreFromEngine, createTurnDraftState, createUploadStore, createWriteTool, decodeFileInputData, defaultAgentDefinition, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, findSafeSplitPointByTurns, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPendingSubagentResults, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, lastAssistantText, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadSkillMetadataFromDirs, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, normalizeToolAccess, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, realResponseText, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, stripPillMetaTokens, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
package/dist/index.js
CHANGED
|
@@ -12772,6 +12772,7 @@ var resolveRunRequest = (conversation, request) => {
|
|
|
12772
12772
|
};
|
|
12773
12773
|
|
|
12774
12774
|
// src/orchestrator/turn.ts
|
|
12775
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
12775
12776
|
var createTurnDraftState = () => ({
|
|
12776
12777
|
assistantResponse: "",
|
|
12777
12778
|
toolTimeline: [],
|
|
@@ -12993,6 +12994,52 @@ var buildApprovalCheckpoints = ({
|
|
|
12993
12994
|
pendingToolCalls,
|
|
12994
12995
|
kind
|
|
12995
12996
|
}));
|
|
12997
|
+
var assembleCheckpointMessages = (conversation, checkpoint) => {
|
|
12998
|
+
const n = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
|
|
12999
|
+
const base = n.baseMessageCount != null ? conversation.messages.slice(0, n.baseMessageCount) : [];
|
|
13000
|
+
return [...base, ...n.checkpointMessages ?? []];
|
|
13001
|
+
};
|
|
13002
|
+
var buildToolResultMessage = (assistantMsg, toolResults) => {
|
|
13003
|
+
if (assistantMsg?.role !== "assistant") return void 0;
|
|
13004
|
+
let toolCalls = [];
|
|
13005
|
+
try {
|
|
13006
|
+
const parsed = JSON.parse(typeof assistantMsg.content === "string" ? assistantMsg.content : "");
|
|
13007
|
+
toolCalls = parsed.tool_calls ?? [];
|
|
13008
|
+
} catch {
|
|
13009
|
+
return void 0;
|
|
13010
|
+
}
|
|
13011
|
+
if (toolCalls.length === 0) return void 0;
|
|
13012
|
+
const provided = new Map(toolResults.map((r) => [r.callId, r]));
|
|
13013
|
+
return {
|
|
13014
|
+
role: "tool",
|
|
13015
|
+
content: JSON.stringify(
|
|
13016
|
+
toolCalls.map((tc) => {
|
|
13017
|
+
const r = provided.get(tc.id);
|
|
13018
|
+
return {
|
|
13019
|
+
type: "tool_result",
|
|
13020
|
+
tool_use_id: tc.id,
|
|
13021
|
+
tool_name: r?.toolName ?? tc.name,
|
|
13022
|
+
content: r ? r.error ? `Tool error: ${r.error}` : JSON.stringify(r.result ?? null) : "Tool error: Tool execution deferred (pending approval checkpoint)"
|
|
13023
|
+
};
|
|
13024
|
+
})
|
|
13025
|
+
),
|
|
13026
|
+
metadata: { timestamp: Date.now(), id: randomUUID6() }
|
|
13027
|
+
};
|
|
13028
|
+
};
|
|
13029
|
+
var buildResumeCheckpoints = ({
|
|
13030
|
+
priorMessages,
|
|
13031
|
+
checkpointEvent,
|
|
13032
|
+
runId,
|
|
13033
|
+
kind = "approval"
|
|
13034
|
+
}) => buildApprovalCheckpoints({
|
|
13035
|
+
approvals: checkpointEvent.approvals,
|
|
13036
|
+
runId,
|
|
13037
|
+
checkpointMessages: [...priorMessages, ...checkpointEvent.checkpointMessages],
|
|
13038
|
+
baseMessageCount: 0,
|
|
13039
|
+
pendingToolCalls: checkpointEvent.pendingToolCalls,
|
|
13040
|
+
kind
|
|
13041
|
+
});
|
|
13042
|
+
var messageText = (m) => typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => p.text ?? "").join("") : "";
|
|
12996
13043
|
var applyTurnMetadata = (conv, meta, opts = {}) => {
|
|
12997
13044
|
const {
|
|
12998
13045
|
clearContinuation = true,
|
|
@@ -13011,6 +13058,20 @@ var applyTurnMetadata = (conv, meta, opts = {}) => {
|
|
|
13011
13058
|
} else if (shouldRebuildCanonical) {
|
|
13012
13059
|
conv._harnessMessages = conv.messages;
|
|
13013
13060
|
}
|
|
13061
|
+
if (isMessageArray(conv._harnessMessages) && conv._harnessMessages.length > 0) {
|
|
13062
|
+
const canonical = conv._harnessMessages;
|
|
13063
|
+
const summarized = canonical.some((m) => m.metadata?.isCompactionSummary);
|
|
13064
|
+
const lastUser = [...conv.messages].reverse().find((m) => m.role === "user");
|
|
13065
|
+
const lastUserText = lastUser ? messageText(lastUser).trim() : "";
|
|
13066
|
+
if (!summarized && lastUserText) {
|
|
13067
|
+
const present = canonical.some((m) => m.role === "user" && messageText(m).trim() === lastUserText);
|
|
13068
|
+
if (!present) {
|
|
13069
|
+
console.error(
|
|
13070
|
+
`[transcript-guard] conversation ${conv.conversationId}: model-facing transcript is missing the latest user message \u2014 it diverged from the display transcript. This is a resume/finalize message-assembly bug; the model will not see that turn's input.`
|
|
13071
|
+
);
|
|
13072
|
+
}
|
|
13073
|
+
}
|
|
13074
|
+
}
|
|
13014
13075
|
if (meta.toolResultArchive !== void 0) {
|
|
13015
13076
|
conv._toolResultArchive = meta.toolResultArchive;
|
|
13016
13077
|
}
|
|
@@ -13041,12 +13102,12 @@ var STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
|
13041
13102
|
import { createLogger as createLogger8, getTextContent as getTextContent3 } from "@poncho-ai/sdk";
|
|
13042
13103
|
|
|
13043
13104
|
// src/orchestrator/entries-dual-write.ts
|
|
13044
|
-
import { randomUUID as
|
|
13105
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
13045
13106
|
var appendEntriesSafe = async (store, conversation, entries, log2) => {
|
|
13046
13107
|
if (entries.length === 0) return [];
|
|
13047
13108
|
try {
|
|
13048
13109
|
const withIds = entries.map(
|
|
13049
|
-
(e) => ({ id:
|
|
13110
|
+
(e) => ({ id: randomUUID7(), ...e })
|
|
13050
13111
|
);
|
|
13051
13112
|
return await store.appendEntries(
|
|
13052
13113
|
conversation.conversationId,
|
|
@@ -13285,34 +13346,11 @@ var AgentOrchestrator = class {
|
|
|
13285
13346
|
});
|
|
13286
13347
|
let latestRunId = conversation.runtimeRunId ?? "";
|
|
13287
13348
|
let checkpointedRun = false;
|
|
13288
|
-
const
|
|
13289
|
-
const
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
if (lastCpMsg?.role === "assistant") {
|
|
13294
|
-
try {
|
|
13295
|
-
const parsed = JSON.parse(typeof lastCpMsg.content === "string" ? lastCpMsg.content : "");
|
|
13296
|
-
const cpToolCalls = parsed.tool_calls ?? [];
|
|
13297
|
-
if (cpToolCalls.length > 0) {
|
|
13298
|
-
const providedMap = new Map(toolResults.map((r) => [r.callId, r]));
|
|
13299
|
-
resumeToolResultMsg = {
|
|
13300
|
-
role: "tool",
|
|
13301
|
-
content: JSON.stringify(cpToolCalls.map((tc) => {
|
|
13302
|
-
const provided = providedMap.get(tc.id);
|
|
13303
|
-
return {
|
|
13304
|
-
type: "tool_result",
|
|
13305
|
-
tool_use_id: tc.id,
|
|
13306
|
-
tool_name: provided?.toolName ?? tc.name,
|
|
13307
|
-
content: provided ? provided.error ? `Tool error: ${provided.error}` : JSON.stringify(provided.result ?? null) : "Tool error: Tool execution deferred (pending approval checkpoint)"
|
|
13308
|
-
};
|
|
13309
|
-
})),
|
|
13310
|
-
metadata: { timestamp: Date.now() }
|
|
13311
|
-
};
|
|
13312
|
-
}
|
|
13313
|
-
} catch {
|
|
13314
|
-
}
|
|
13315
|
-
}
|
|
13349
|
+
const fullCheckpointMessages = assembleCheckpointMessages(conversation, checkpoint);
|
|
13350
|
+
const resumeToolResultMsg = buildToolResultMessage(
|
|
13351
|
+
fullCheckpointMessages[fullCheckpointMessages.length - 1],
|
|
13352
|
+
toolResults
|
|
13353
|
+
);
|
|
13316
13354
|
const fullCheckpointWithResults = resumeToolResultMsg ? [...fullCheckpointMessages, resumeToolResultMsg] : fullCheckpointMessages;
|
|
13317
13355
|
let draftRef;
|
|
13318
13356
|
let execution;
|
|
@@ -13357,12 +13395,10 @@ var AgentOrchestrator = class {
|
|
|
13357
13395
|
const cpEvent = event;
|
|
13358
13396
|
const conv = await this.conversationStore.get(conversationId);
|
|
13359
13397
|
if (conv) {
|
|
13360
|
-
conv.pendingApprovals =
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
baseMessageCount: 0,
|
|
13365
|
-
pendingToolCalls: cpEvent.pendingToolCalls
|
|
13398
|
+
conv.pendingApprovals = buildResumeCheckpoints({
|
|
13399
|
+
priorMessages: fullCheckpointWithResults,
|
|
13400
|
+
checkpointEvent: cpEvent,
|
|
13401
|
+
runId: latestRunId
|
|
13366
13402
|
});
|
|
13367
13403
|
conv.updatedAt = Date.now();
|
|
13368
13404
|
await this.conversationStore.update(conv);
|
|
@@ -14610,7 +14646,7 @@ ${resultBody}`,
|
|
|
14610
14646
|
};
|
|
14611
14647
|
|
|
14612
14648
|
// src/orchestrator/run-conversation-turn.ts
|
|
14613
|
-
import { randomUUID as
|
|
14649
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
14614
14650
|
import { createLogger as createLogger9 } from "@poncho-ai/sdk";
|
|
14615
14651
|
var log = createLogger9("orchestrator");
|
|
14616
14652
|
var runConversationTurn = async (opts) => {
|
|
@@ -14650,9 +14686,9 @@ var runConversationTurn = async (opts) => {
|
|
|
14650
14686
|
const userMessage = {
|
|
14651
14687
|
role: "user",
|
|
14652
14688
|
content: userContent,
|
|
14653
|
-
metadata: { id:
|
|
14689
|
+
metadata: { id: randomUUID8(), timestamp: turnTimestamp }
|
|
14654
14690
|
};
|
|
14655
|
-
const assistantId =
|
|
14691
|
+
const assistantId = randomUUID8();
|
|
14656
14692
|
const draft = createTurnDraftState();
|
|
14657
14693
|
let latestRunId = conversation.runtimeRunId ?? "";
|
|
14658
14694
|
let runCancelled = false;
|
|
@@ -15028,11 +15064,14 @@ export {
|
|
|
15028
15064
|
abnormalEndResponse,
|
|
15029
15065
|
appendEntriesSafe,
|
|
15030
15066
|
applyTurnMetadata,
|
|
15067
|
+
assembleCheckpointMessages,
|
|
15031
15068
|
buildAgentDirectoryName,
|
|
15032
15069
|
buildApprovalCheckpoints,
|
|
15033
15070
|
buildAssistantMetadata,
|
|
15071
|
+
buildResumeCheckpoints,
|
|
15034
15072
|
buildSkillContextWindow,
|
|
15035
15073
|
buildToolCompletedText,
|
|
15074
|
+
buildToolResultMessage,
|
|
15036
15075
|
cloneSections,
|
|
15037
15076
|
compactMessages,
|
|
15038
15077
|
completeOpenAICodexDeviceAuth,
|
package/package.json
CHANGED
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
applyTurnMetadata,
|
|
11
11
|
normalizeApprovalCheckpoint,
|
|
12
12
|
buildApprovalCheckpoints,
|
|
13
|
+
assembleCheckpointMessages,
|
|
14
|
+
buildToolResultMessage,
|
|
15
|
+
buildResumeCheckpoints,
|
|
13
16
|
createTurnDraftState,
|
|
14
17
|
recordStandardTurnEvent,
|
|
15
18
|
type TurnDraftState,
|
|
@@ -421,38 +424,11 @@ export class AgentOrchestrator {
|
|
|
421
424
|
let latestRunId = conversation.runtimeRunId ?? "";
|
|
422
425
|
let checkpointedRun = false;
|
|
423
426
|
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
let resumeToolResultMsg: Message | undefined;
|
|
431
|
-
const lastCpMsg = fullCheckpointMessages[fullCheckpointMessages.length - 1];
|
|
432
|
-
if (lastCpMsg?.role === "assistant") {
|
|
433
|
-
try {
|
|
434
|
-
const parsed = JSON.parse(typeof lastCpMsg.content === "string" ? lastCpMsg.content : "");
|
|
435
|
-
const cpToolCalls: Array<{ id: string; name: string }> = parsed.tool_calls ?? [];
|
|
436
|
-
if (cpToolCalls.length > 0) {
|
|
437
|
-
const providedMap = new Map(toolResults.map(r => [r.callId, r]));
|
|
438
|
-
resumeToolResultMsg = {
|
|
439
|
-
role: "tool",
|
|
440
|
-
content: JSON.stringify(cpToolCalls.map(tc => {
|
|
441
|
-
const provided = providedMap.get(tc.id);
|
|
442
|
-
return {
|
|
443
|
-
type: "tool_result",
|
|
444
|
-
tool_use_id: tc.id,
|
|
445
|
-
tool_name: provided?.toolName ?? tc.name,
|
|
446
|
-
content: provided
|
|
447
|
-
? (provided.error ? `Tool error: ${provided.error}` : JSON.stringify(provided.result ?? null))
|
|
448
|
-
: "Tool error: Tool execution deferred (pending approval checkpoint)",
|
|
449
|
-
};
|
|
450
|
-
})),
|
|
451
|
-
metadata: { timestamp: Date.now() },
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
} catch { /* last message is not a parseable assistant-with-tools -- skip */ }
|
|
455
|
-
}
|
|
427
|
+
const fullCheckpointMessages = assembleCheckpointMessages(conversation, checkpoint);
|
|
428
|
+
const resumeToolResultMsg = buildToolResultMessage(
|
|
429
|
+
fullCheckpointMessages[fullCheckpointMessages.length - 1],
|
|
430
|
+
toolResults,
|
|
431
|
+
);
|
|
456
432
|
const fullCheckpointWithResults = resumeToolResultMsg
|
|
457
433
|
? [...fullCheckpointMessages, resumeToolResultMsg]
|
|
458
434
|
: fullCheckpointMessages;
|
|
@@ -505,12 +481,10 @@ export class AgentOrchestrator {
|
|
|
505
481
|
};
|
|
506
482
|
const conv = await this.conversationStore.get(conversationId);
|
|
507
483
|
if (conv) {
|
|
508
|
-
conv.pendingApprovals =
|
|
509
|
-
|
|
484
|
+
conv.pendingApprovals = buildResumeCheckpoints({
|
|
485
|
+
priorMessages: fullCheckpointWithResults,
|
|
486
|
+
checkpointEvent: cpEvent,
|
|
510
487
|
runId: latestRunId,
|
|
511
|
-
checkpointMessages: [...fullCheckpointWithResults, ...cpEvent.checkpointMessages],
|
|
512
|
-
baseMessageCount: 0,
|
|
513
|
-
pendingToolCalls: cpEvent.pendingToolCalls,
|
|
514
488
|
});
|
|
515
489
|
conv.updatedAt = Date.now();
|
|
516
490
|
await this.conversationStore.update(conv);
|
package/src/orchestrator/turn.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import type { AgentEvent, Message } from "@poncho-ai/sdk";
|
|
2
3
|
import type { Conversation } from "../state.js";
|
|
3
4
|
import type { AgentHarness } from "../harness.js";
|
|
@@ -341,6 +342,110 @@ export const buildApprovalCheckpoints = ({
|
|
|
341
342
|
kind,
|
|
342
343
|
}));
|
|
343
344
|
|
|
345
|
+
// ── Checkpoint transcript reconstruction (single source of truth) ──
|
|
346
|
+
//
|
|
347
|
+
// Resuming a checkpointed turn requires rebuilding the FULL canonical model
|
|
348
|
+
// transcript from what was stored at the pause. Getting this right — in the
|
|
349
|
+
// canonical message space, not the display one — is subtle, and re-deriving
|
|
350
|
+
// it by hand in each embedder is exactly how the model-facing transcript
|
|
351
|
+
// drifted from the display transcript and silently dropped a turn's user
|
|
352
|
+
// message after an approval. These helpers are that logic, exported so the
|
|
353
|
+
// orchestrator's own resume AND external embedders (e.g. PonchOS, which
|
|
354
|
+
// executes gated tools itself) share one implementation that can't drift.
|
|
355
|
+
|
|
356
|
+
/** Reconstruct the full canonical transcript a checkpoint resumes from: the
|
|
357
|
+
* prior history (before the checkpointed turn) + the checkpoint delta.
|
|
358
|
+
* Handles both storage conventions via `normalizeApprovalCheckpoint` — the
|
|
359
|
+
* initial checkpoint (base = prior length, delta) and a resume-created one
|
|
360
|
+
* (base 0, full canonical). */
|
|
361
|
+
export const assembleCheckpointMessages = (
|
|
362
|
+
conversation: Conversation,
|
|
363
|
+
checkpoint: StoredApproval,
|
|
364
|
+
): Message[] => {
|
|
365
|
+
const n = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
|
|
366
|
+
const base = n.baseMessageCount != null ? conversation.messages.slice(0, n.baseMessageCount) : [];
|
|
367
|
+
return [...base, ...(n.checkpointMessages ?? [])];
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/** Pair an assistant tool-call message's `tool_calls` with externally-computed
|
|
371
|
+
* results into the single `role:"tool"` message a continuation reads.
|
|
372
|
+
* Returns undefined when `assistantMsg` isn't an assistant message with
|
|
373
|
+
* parseable tool calls. Used to persist the resume's canonical history so it
|
|
374
|
+
* matches what `continueFromToolResult` fed the model. Missing results
|
|
375
|
+
* default to the deferred-error marker (same text the run uses). */
|
|
376
|
+
export const buildToolResultMessage = (
|
|
377
|
+
assistantMsg: Message | undefined,
|
|
378
|
+
toolResults: Array<{ callId: string; toolName: string; result?: unknown; error?: string }>,
|
|
379
|
+
): Message | undefined => {
|
|
380
|
+
if (assistantMsg?.role !== "assistant") return undefined;
|
|
381
|
+
let toolCalls: Array<{ id: string; name: string }> = [];
|
|
382
|
+
try {
|
|
383
|
+
const parsed = JSON.parse(typeof assistantMsg.content === "string" ? assistantMsg.content : "");
|
|
384
|
+
toolCalls = parsed.tool_calls ?? [];
|
|
385
|
+
} catch {
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
if (toolCalls.length === 0) return undefined;
|
|
389
|
+
const provided = new Map(toolResults.map((r) => [r.callId, r]));
|
|
390
|
+
return {
|
|
391
|
+
role: "tool",
|
|
392
|
+
content: JSON.stringify(
|
|
393
|
+
toolCalls.map((tc) => {
|
|
394
|
+
const r = provided.get(tc.id);
|
|
395
|
+
return {
|
|
396
|
+
type: "tool_result",
|
|
397
|
+
tool_use_id: tc.id,
|
|
398
|
+
tool_name: r?.toolName ?? tc.name,
|
|
399
|
+
content: r
|
|
400
|
+
? (r.error ? `Tool error: ${r.error}` : JSON.stringify(r.result ?? null))
|
|
401
|
+
: "Tool error: Tool execution deferred (pending approval checkpoint)",
|
|
402
|
+
};
|
|
403
|
+
}),
|
|
404
|
+
),
|
|
405
|
+
metadata: { timestamp: Date.now(), id: randomUUID() },
|
|
406
|
+
};
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/** Build the `pendingApprovals` rows for a checkpoint reached DURING a resume
|
|
410
|
+
* continuation. Stores the WHOLE canonical history the continuation ran with
|
|
411
|
+
* (`priorMessages` = prior + tool result + new delta) with
|
|
412
|
+
* `baseMessageCount: 0`. Keeping the full history here — rather than a delta
|
|
413
|
+
* + a base count into a different message array — is what lets the next
|
|
414
|
+
* resume reconstruct with no index arithmetic. */
|
|
415
|
+
export const buildResumeCheckpoints = ({
|
|
416
|
+
priorMessages,
|
|
417
|
+
checkpointEvent,
|
|
418
|
+
runId,
|
|
419
|
+
kind = "approval",
|
|
420
|
+
}: {
|
|
421
|
+
priorMessages: Message[];
|
|
422
|
+
checkpointEvent: {
|
|
423
|
+
approvals: ApprovalEventItem[];
|
|
424
|
+
checkpointMessages: Message[];
|
|
425
|
+
pendingToolCalls: PendingToolCall[];
|
|
426
|
+
};
|
|
427
|
+
runId: string;
|
|
428
|
+
kind?: "approval" | "device";
|
|
429
|
+
}): NonNullable<Conversation["pendingApprovals"]> =>
|
|
430
|
+
buildApprovalCheckpoints({
|
|
431
|
+
approvals: checkpointEvent.approvals,
|
|
432
|
+
runId,
|
|
433
|
+
checkpointMessages: [...priorMessages, ...checkpointEvent.checkpointMessages],
|
|
434
|
+
baseMessageCount: 0,
|
|
435
|
+
pendingToolCalls: checkpointEvent.pendingToolCalls,
|
|
436
|
+
kind,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
/** Text of a message for the transcript-integrity guard — flattens the two
|
|
440
|
+
* content shapes so a user message can be matched across the display and
|
|
441
|
+
* canonical transcripts regardless of how each stored it. */
|
|
442
|
+
const messageText = (m: Message): string =>
|
|
443
|
+
typeof m.content === "string"
|
|
444
|
+
? m.content
|
|
445
|
+
: Array.isArray(m.content)
|
|
446
|
+
? m.content.map((p) => (p as { text?: string }).text ?? "").join("")
|
|
447
|
+
: "";
|
|
448
|
+
|
|
344
449
|
// ── Turn metadata persistence ──
|
|
345
450
|
|
|
346
451
|
export const applyTurnMetadata = (
|
|
@@ -373,6 +478,31 @@ export const applyTurnMetadata = (
|
|
|
373
478
|
conv._harnessMessages = conv.messages;
|
|
374
479
|
}
|
|
375
480
|
|
|
481
|
+
// Invariant guard: the model-facing transcript must not drop the current
|
|
482
|
+
// turn's user message. A resume that reconstructed `_harnessMessages`
|
|
483
|
+
// incorrectly silently lost it (the display transcript kept it, the model
|
|
484
|
+
// didn't), so the agent second-guessed an approval it never saw. This is the
|
|
485
|
+
// one choke point every finalize flows through — assert the latest user
|
|
486
|
+
// message survived into canonical, and log loudly if not. Matched by content
|
|
487
|
+
// (robust across the display/canonical shapes) and skipped when compaction
|
|
488
|
+
// has legitimately summarized history. Log-only; never throws.
|
|
489
|
+
if (isMessageArray(conv._harnessMessages) && conv._harnessMessages.length > 0) {
|
|
490
|
+
const canonical = conv._harnessMessages;
|
|
491
|
+
const summarized = canonical.some((m) => m.metadata?.isCompactionSummary);
|
|
492
|
+
const lastUser = [...conv.messages].reverse().find((m) => m.role === "user");
|
|
493
|
+
const lastUserText = lastUser ? messageText(lastUser).trim() : "";
|
|
494
|
+
if (!summarized && lastUserText) {
|
|
495
|
+
const present = canonical.some((m) => m.role === "user" && messageText(m).trim() === lastUserText);
|
|
496
|
+
if (!present) {
|
|
497
|
+
console.error(
|
|
498
|
+
`[transcript-guard] conversation ${conv.conversationId}: model-facing transcript ` +
|
|
499
|
+
`is missing the latest user message — it diverged from the display transcript. ` +
|
|
500
|
+
`This is a resume/finalize message-assembly bug; the model will not see that turn's input.`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
376
506
|
if (meta.toolResultArchive !== undefined) {
|
|
377
507
|
conv._toolResultArchive = meta.toolResultArchive;
|
|
378
508
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { Message } from "@poncho-ai/sdk";
|
|
3
|
+
import {
|
|
4
|
+
assembleCheckpointMessages,
|
|
5
|
+
buildToolResultMessage,
|
|
6
|
+
buildResumeCheckpoints,
|
|
7
|
+
applyTurnMetadata,
|
|
8
|
+
type StoredApproval,
|
|
9
|
+
} from "../src/orchestrator/index.js";
|
|
10
|
+
import type { Conversation } from "../src/state.js";
|
|
11
|
+
|
|
12
|
+
const conv = (overrides: Partial<Conversation> & Pick<Conversation, "messages">): Conversation => ({
|
|
13
|
+
conversationId: "conv_test",
|
|
14
|
+
title: "test",
|
|
15
|
+
ownerId: "owner",
|
|
16
|
+
tenantId: null,
|
|
17
|
+
createdAt: Date.now(),
|
|
18
|
+
updatedAt: Date.now(),
|
|
19
|
+
...overrides,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// An assistant tool-call message is JSON `{ text, tool_calls: [...] }` in the
|
|
23
|
+
// canonical transcript.
|
|
24
|
+
const assistantToolCall = (text: string, calls: Array<{ id: string; name: string }>): Message => ({
|
|
25
|
+
role: "assistant",
|
|
26
|
+
content: JSON.stringify({ text, tool_calls: calls.map((c) => ({ ...c, input: {} })) }),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const meta = (harnessMessages?: Message[]) => ({
|
|
30
|
+
latestRunId: "",
|
|
31
|
+
contextTokens: 0,
|
|
32
|
+
contextWindow: 0,
|
|
33
|
+
harnessMessages,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe("assembleCheckpointMessages", () => {
|
|
37
|
+
it("initial checkpoint: prior history (sliced by base) + delta", () => {
|
|
38
|
+
const prior: Message[] = [
|
|
39
|
+
{ role: "user", content: "hi" },
|
|
40
|
+
{ role: "assistant", content: "hello" },
|
|
41
|
+
];
|
|
42
|
+
// Task-bearing turn: the delta carries the turn's user message + the
|
|
43
|
+
// assistant preamble/tool-call. base = prior length.
|
|
44
|
+
const delta: Message[] = [
|
|
45
|
+
{ role: "user", content: "yeah those are a lot better" },
|
|
46
|
+
assistantToolCall("I found the two PE events…", [{ id: "call_1", name: "update_event" }]),
|
|
47
|
+
];
|
|
48
|
+
const checkpoint = { baseMessageCount: prior.length, checkpointMessages: delta } as StoredApproval;
|
|
49
|
+
const out = assembleCheckpointMessages(conv({ messages: prior }), checkpoint);
|
|
50
|
+
|
|
51
|
+
expect(out).toHaveLength(4);
|
|
52
|
+
expect(out.some((m) => m.role === "user" && m.content === "yeah those are a lot better")).toBe(true);
|
|
53
|
+
expect(out.some((m) => typeof m.content === "string" && m.content.includes("I found the two PE events"))).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("resume convention: base 0 + full canonical returns the full canonical verbatim", () => {
|
|
57
|
+
const full: Message[] = [
|
|
58
|
+
{ role: "user", content: "hi" },
|
|
59
|
+
{ role: "user", content: "approve this" },
|
|
60
|
+
assistantToolCall("preamble", [{ id: "call_1", name: "t" }]),
|
|
61
|
+
];
|
|
62
|
+
const checkpoint = { baseMessageCount: 0, checkpointMessages: full } as StoredApproval;
|
|
63
|
+
// Display messages are irrelevant when base is 0 — the reconstruction must
|
|
64
|
+
// NOT depend on them (this is what the old PonchOS hand-merge got wrong).
|
|
65
|
+
const out = assembleCheckpointMessages(conv({ messages: [{ role: "user", content: "unrelated display" }] }), checkpoint);
|
|
66
|
+
expect(out).toEqual(full);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("buildToolResultMessage", () => {
|
|
71
|
+
const asst = assistantToolCall("", [{ id: "call_1", name: "update_event" }]);
|
|
72
|
+
|
|
73
|
+
it("pairs a provided result by callId", () => {
|
|
74
|
+
const msg = buildToolResultMessage(asst, [{ callId: "call_1", toolName: "update_event", result: { ok: true } }]);
|
|
75
|
+
expect(msg?.role).toBe("tool");
|
|
76
|
+
expect(msg?.content).toContain("call_1");
|
|
77
|
+
expect(msg?.content).toContain("ok");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("emits the deferred-error marker when a result is missing", () => {
|
|
81
|
+
const msg = buildToolResultMessage(asst, []);
|
|
82
|
+
expect(msg?.content).toContain("Tool execution deferred");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns undefined for a non-assistant / non-tool-call message", () => {
|
|
86
|
+
expect(buildToolResultMessage({ role: "user", content: "hi" }, [])).toBeUndefined();
|
|
87
|
+
expect(buildToolResultMessage({ role: "assistant", content: "plain text" }, [])).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("buildResumeCheckpoints + round-trip (the regression)", () => {
|
|
92
|
+
it("stores full canonical (base 0) so the NEXT resume reconstructs without dropping the user turn", () => {
|
|
93
|
+
// The continuation ran with this full canonical (incl. the turn's user
|
|
94
|
+
// message + preamble) and appended a tool result.
|
|
95
|
+
const fullWithResults: Message[] = [
|
|
96
|
+
{ role: "user", content: "hi" },
|
|
97
|
+
{ role: "assistant", content: "hello" },
|
|
98
|
+
{ role: "user", content: "approve this" },
|
|
99
|
+
assistantToolCall("preamble text", [{ id: "call_1", name: "t1" }]),
|
|
100
|
+
{ role: "tool", content: JSON.stringify([{ type: "tool_result", tool_use_id: "call_1", content: "done" }]) },
|
|
101
|
+
];
|
|
102
|
+
// It then gated again on a new tool (the re-checkpoint event delta).
|
|
103
|
+
const delta2: Message[] = [assistantToolCall("", [{ id: "call_2", name: "t2" }])];
|
|
104
|
+
const stored = buildResumeCheckpoints({
|
|
105
|
+
priorMessages: fullWithResults,
|
|
106
|
+
checkpointEvent: {
|
|
107
|
+
approvals: [{ approvalId: "a2", tool: "t2", toolCallId: "call_2", input: {} }],
|
|
108
|
+
checkpointMessages: delta2,
|
|
109
|
+
pendingToolCalls: [{ id: "call_2", name: "t2", input: {} }],
|
|
110
|
+
},
|
|
111
|
+
runId: "run_2",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(stored[0]!.baseMessageCount).toBe(0);
|
|
115
|
+
expect(stored[0]!.checkpointMessages).toEqual([...fullWithResults, ...delta2]);
|
|
116
|
+
|
|
117
|
+
// Next resume reconstructs from the stored checkpoint — display is
|
|
118
|
+
// deliberately unrelated to prove no dependence on it.
|
|
119
|
+
const reconstructed = assembleCheckpointMessages(
|
|
120
|
+
conv({ messages: [{ role: "user", content: "unrelated" }], pendingApprovals: stored }),
|
|
121
|
+
stored[0]!,
|
|
122
|
+
);
|
|
123
|
+
// The user turn + preamble that PonchOS used to drop MUST survive.
|
|
124
|
+
expect(reconstructed.some((m) => m.role === "user" && m.content === "approve this")).toBe(true);
|
|
125
|
+
expect(reconstructed.some((m) => typeof m.content === "string" && m.content.includes("preamble text"))).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("applyTurnMetadata transcript-integrity guard", () => {
|
|
130
|
+
const latestUser: Message = { role: "user", content: "the latest question", metadata: { id: "u_latest" } };
|
|
131
|
+
|
|
132
|
+
it("logs when the latest user message is missing from the canonical transcript", () => {
|
|
133
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
134
|
+
const c = conv({ messages: [{ role: "assistant", content: "earlier" }, latestUser] });
|
|
135
|
+
// Canonical is missing "the latest question" — the exact divergence.
|
|
136
|
+
applyTurnMetadata(c, meta([{ role: "user", content: "hi" }, { role: "assistant", content: "x" }]));
|
|
137
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining("[transcript-guard]"));
|
|
138
|
+
spy.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("stays silent when the latest user message is present in canonical", () => {
|
|
142
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
143
|
+
const c = conv({ messages: [latestUser] });
|
|
144
|
+
applyTurnMetadata(c, meta([{ role: "user", content: "the latest question" }, { role: "assistant", content: "reply" }]));
|
|
145
|
+
expect(spy).not.toHaveBeenCalled();
|
|
146
|
+
spy.mockRestore();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("skips the check when canonical was legitimately compacted", () => {
|
|
150
|
+
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
151
|
+
const c = conv({ messages: [latestUser] });
|
|
152
|
+
applyTurnMetadata(c, meta([{ role: "assistant", content: "summary", metadata: { isCompactionSummary: true } }]));
|
|
153
|
+
expect(spy).not.toHaveBeenCalled();
|
|
154
|
+
spy.mockRestore();
|
|
155
|
+
});
|
|
156
|
+
});
|