@poncho-ai/harness 0.41.0 → 0.42.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 +51 -0
- package/dist/index.d.ts +48 -2
- package/dist/index.js +310 -0
- package/package.json +1 -1
- package/src/harness.ts +7 -0
- package/src/orchestrator/index.ts +6 -0
- package/src/orchestrator/run-conversation-turn.ts +420 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/harness@0.
|
|
2
|
+
> @poncho-ai/harness@0.42.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[
|
|
11
|
+
[32mESM[39m [1mdist/index.js [22m[32m504.78 KB[39m
|
|
12
12
|
[32mESM[39m [1mdist/isolate-VY35DGLM.js [22m[32m49.43 KB[39m
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 228ms
|
|
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 7856ms
|
|
16
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m79.11 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
# @poncho-ai/harness
|
|
2
2
|
|
|
3
|
+
## 0.42.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`39793b0`](https://github.com/cesr/poncho-ai/commit/39793b0ab11ed26f140af6fc9c0cd3e1b1c83fec) Thanks [@cesr](https://github.com/cesr)! - harness: extract `runConversationTurn` helper; refactor CLI to use it
|
|
8
|
+
|
|
9
|
+
Lifts the inline turn lifecycle from the CLI's
|
|
10
|
+
`POST /api/conversations/:id/messages` handler (~280 lines of orchestration)
|
|
11
|
+
into a new public helper at `@poncho-ai/harness`.
|
|
12
|
+
|
|
13
|
+
The helper handles the full conversation lifecycle for a primary chat
|
|
14
|
+
turn: load the conversation with archive, resolve canonical history,
|
|
15
|
+
upload files via the harness's upload store, build stable user/assistant
|
|
16
|
+
ids, persist the user message immediately, drive `executeConversationTurn`,
|
|
17
|
+
periodically persist the in-flight assistant draft on `step:completed`
|
|
18
|
+
and `tool:approval:required`, persist on `tool:approval:checkpoint` and
|
|
19
|
+
`run:completed` continuation, rebuild history on `compaction:completed`,
|
|
20
|
+
apply turn metadata on success, and persist partial state on
|
|
21
|
+
cancel/error.
|
|
22
|
+
|
|
23
|
+
Caller responsibilities (auth, active-run dedup, streaming, continuation
|
|
24
|
+
HTTP self-fetch, title inference) stay outside the helper — passed in
|
|
25
|
+
via opts or handled around the call. `opts.onEvent` is invoked for every
|
|
26
|
+
`AgentEvent` for downstream forwarding (SSE, WebSocket, telemetry, etc.).
|
|
27
|
+
|
|
28
|
+
The CLI's handler now delegates to `runConversationTurn` (drops from
|
|
29
|
+
~430 to ~150 lines). Consumers like PonchOS can call the same helper
|
|
30
|
+
to ship the _exact_ same conversation lifecycle without duplicating
|
|
31
|
+
the orchestration.
|
|
32
|
+
|
|
33
|
+
Public API additions:
|
|
34
|
+
- `runConversationTurn(opts): Promise<RunConversationTurnResult>`
|
|
35
|
+
- `RunConversationTurnOpts`
|
|
36
|
+
- `RunConversationTurnResult`
|
|
37
|
+
|
|
38
|
+
No behavior changes. The helper is a verbatim extraction of the CLI's
|
|
39
|
+
prior inline implementation.
|
|
40
|
+
|
|
41
|
+
### Patch Changes
|
|
42
|
+
|
|
43
|
+
- [`111d24e`](https://github.com/cesr/poncho-ai/commit/111d24efaab054ef7543c396085f8f4d41e7976a) Thanks [@cesr](https://github.com/cesr)! - cli: include VFS skills in the chat input slash command menu
|
|
44
|
+
|
|
45
|
+
The `/api/slash-commands` endpoint was returning only repo-loaded skills,
|
|
46
|
+
so tenant-authored skills stored in the VFS (`/skills/<name>/SKILL.md`)
|
|
47
|
+
never appeared in the `/` autocomplete bar even though the agent could
|
|
48
|
+
already see and run them at conversation time.
|
|
49
|
+
|
|
50
|
+
The endpoint now resolves skills per-tenant via a new
|
|
51
|
+
`harness.listSkillsForTenant(tenantId)` and applies the same repo-wins
|
|
52
|
+
collision semantics used elsewhere in the harness.
|
|
53
|
+
|
|
3
54
|
## 0.41.0
|
|
4
55
|
|
|
5
56
|
### Minor Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LanguageModel } from 'ai';
|
|
2
2
|
import * as _poncho_ai_sdk from '@poncho-ai/sdk';
|
|
3
|
-
import { Message, ToolContext, ToolDefinition, JsonSchema, RunResult, AgentFailure, RunInput, AgentEvent } from '@poncho-ai/sdk';
|
|
3
|
+
import { Message, ToolContext, ToolDefinition, JsonSchema, RunResult, AgentFailure, RunInput, AgentEvent, FileInput } from '@poncho-ai/sdk';
|
|
4
4
|
export { ToolDefinition, defineTool } from '@poncho-ai/sdk';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { IFileSystem, BufferEncoding, FsStat, FileContent, MkdirOptions, RmOptions, CpOptions, Bash } from 'just-bash';
|
|
@@ -1186,6 +1186,10 @@ declare class AgentHarness {
|
|
|
1186
1186
|
name: string;
|
|
1187
1187
|
description: string;
|
|
1188
1188
|
}>;
|
|
1189
|
+
listSkillsForTenant(tenantId: string | undefined | null): Promise<Array<{
|
|
1190
|
+
name: string;
|
|
1191
|
+
description: string;
|
|
1192
|
+
}>>;
|
|
1189
1193
|
/**
|
|
1190
1194
|
* Wraps the run() generator with an OTel root span (invoke_agent) so all
|
|
1191
1195
|
* child spans (LLM calls via AI SDK, tool execution) group under one trace.
|
|
@@ -1879,4 +1883,46 @@ declare class AgentOrchestrator {
|
|
|
1879
1883
|
recoverStaleSubagents(): Promise<void>;
|
|
1880
1884
|
}
|
|
1881
1885
|
|
|
1882
|
-
|
|
1886
|
+
interface RunConversationTurnOpts {
|
|
1887
|
+
/** Initialised harness instance. */
|
|
1888
|
+
harness: AgentHarness;
|
|
1889
|
+
/** Conversation store backing the turn (typically `engine.conversations` from a StorageEngine). */
|
|
1890
|
+
conversationStore: ConversationStore;
|
|
1891
|
+
conversationId: string;
|
|
1892
|
+
/** The user's new message text. Required (use `""` if you only want to attach files). */
|
|
1893
|
+
task: string;
|
|
1894
|
+
/**
|
|
1895
|
+
* Optional file attachments (FileInput.data is base64 / data URI / https URL).
|
|
1896
|
+
* Files are uploaded via `harness.uploadStore` first so the persisted user
|
|
1897
|
+
* message references stable URLs instead of fat base64 blobs.
|
|
1898
|
+
*/
|
|
1899
|
+
files?: FileInput[];
|
|
1900
|
+
/**
|
|
1901
|
+
* Extra parameters merged into runInput.parameters. Use this for recall
|
|
1902
|
+
* corpus, archive lookup keys, messaging metadata, etc. Do NOT include
|
|
1903
|
+
* `__activeConversationId`, `__ownerId`, or the tool-result-archive — the
|
|
1904
|
+
* helper sets those itself.
|
|
1905
|
+
*/
|
|
1906
|
+
parameters?: Record<string, unknown>;
|
|
1907
|
+
abortSignal?: AbortSignal;
|
|
1908
|
+
tenantId?: string | null;
|
|
1909
|
+
/** Per-event hook — called for every AgentEvent yielded by the run, in order. */
|
|
1910
|
+
onEvent?: (event: AgentEvent) => void | Promise<void>;
|
|
1911
|
+
}
|
|
1912
|
+
interface RunConversationTurnResult {
|
|
1913
|
+
/** runId of the most recent run started during this turn. */
|
|
1914
|
+
latestRunId: string;
|
|
1915
|
+
/** True if the run was cancelled (via abortSignal or run:cancelled event). */
|
|
1916
|
+
cancelled: boolean;
|
|
1917
|
+
/** True if the run errored. The error has been emitted via onEvent as run:error. */
|
|
1918
|
+
errored: boolean;
|
|
1919
|
+
/** True if the run requested a continuation. Caller is responsible for triggering the continuation. */
|
|
1920
|
+
continuation: boolean;
|
|
1921
|
+
/** True if the run paused at a tool-approval checkpoint. */
|
|
1922
|
+
checkpointed: boolean;
|
|
1923
|
+
contextTokens: number;
|
|
1924
|
+
contextWindow: number;
|
|
1925
|
+
}
|
|
1926
|
+
declare const runConversationTurn: (opts: RunConversationTurnOpts) => Promise<RunConversationTurnResult>;
|
|
1927
|
+
|
|
1928
|
+
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 CompactMessagesOptions, type CompactResult, type CompactionConfig, type ContinuationHooks, type Conversation, type ConversationCreateInit, type ConversationState, type ConversationStatusSnapshot, type ConversationStore, type ConversationSummary, type CreateSkillToolsOptions, type CronJobConfig, 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 NetworkConfig, 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 SubagentSpawnResult, type SubagentSummary, 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, 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, deleteOpenAICodexSession, deriveUploadKey, ensureAgentIdentity, estimateTokens, estimateTotalTokens, executeConversationTurn, findSafeSplitPoint, flushTurnDraft, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getOpenAICodexAccessToken, getOpenAICodexAuthFilePath, getOpenAICodexRequiredScopes, getPonchoStoreRoot, isMessageArray, jsonSchemaToZod, loadCanonicalHistory, loadPonchoConfig, loadRunHistory, loadSkillContext, loadSkillInstructions, loadSkillMetadata, loadVfsSkillMetadata, mergeSkills, normalizeApprovalCheckpoint, normalizeOtlp, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, parseSkillFrontmatter, ponchoDocsTool, readOpenAICodexSession, readSkillResource, recordStandardTurnEvent, renderAgentPrompt, resolveAgentIdentity, resolveCompactionConfig, resolveEnv, resolveMemoryConfig, resolveRunRequest, resolveSkillDirs, resolveStateConfig, runConversationTurn, slugifyStorageComponent, startOpenAICodexDeviceAuth, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
|
package/dist/index.js
CHANGED
|
@@ -9654,6 +9654,10 @@ var AgentHarness = class _AgentHarness {
|
|
|
9654
9654
|
listSkills() {
|
|
9655
9655
|
return this.loadedSkills.map((s) => ({ name: s.name, description: s.description }));
|
|
9656
9656
|
}
|
|
9657
|
+
async listSkillsForTenant(tenantId) {
|
|
9658
|
+
const skills = await this.getSkillsForTenant(tenantId);
|
|
9659
|
+
return skills.map((s) => ({ name: s.name, description: s.description }));
|
|
9660
|
+
}
|
|
9657
9661
|
/**
|
|
9658
9662
|
* Wraps the run() generator with an OTel root span (invoke_agent) so all
|
|
9659
9663
|
* child spans (LLM calls via AI SDK, tool execution) group under one trace.
|
|
@@ -12991,6 +12995,311 @@ ${resultBody}`,
|
|
|
12991
12995
|
}
|
|
12992
12996
|
};
|
|
12993
12997
|
|
|
12998
|
+
// src/orchestrator/run-conversation-turn.ts
|
|
12999
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
13000
|
+
import { createLogger as createLogger7 } from "@poncho-ai/sdk";
|
|
13001
|
+
var log = createLogger7("orchestrator");
|
|
13002
|
+
var runConversationTurn = async (opts) => {
|
|
13003
|
+
const conversation = await opts.conversationStore.getWithArchive(opts.conversationId);
|
|
13004
|
+
if (!conversation) {
|
|
13005
|
+
throw new Error(`Conversation not found: ${opts.conversationId}`);
|
|
13006
|
+
}
|
|
13007
|
+
const canonicalHistory = resolveRunRequest(conversation, {
|
|
13008
|
+
conversationId: opts.conversationId,
|
|
13009
|
+
messages: conversation.messages
|
|
13010
|
+
});
|
|
13011
|
+
const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
|
|
13012
|
+
const harnessMessages = [...canonicalHistory.messages];
|
|
13013
|
+
const historyMessages = [...conversation.messages];
|
|
13014
|
+
const preRunMessages = [...conversation.messages];
|
|
13015
|
+
let userContent = opts.task;
|
|
13016
|
+
if (opts.files && opts.files.length > 0 && opts.harness.uploadStore) {
|
|
13017
|
+
const uploadedParts = await Promise.all(
|
|
13018
|
+
opts.files.map(async (f) => {
|
|
13019
|
+
const buf = Buffer.from(f.data, "base64");
|
|
13020
|
+
const key = deriveUploadKey(buf, f.mediaType);
|
|
13021
|
+
const ref = await opts.harness.uploadStore.put(key, buf, f.mediaType);
|
|
13022
|
+
return {
|
|
13023
|
+
type: "file",
|
|
13024
|
+
data: ref,
|
|
13025
|
+
mediaType: f.mediaType,
|
|
13026
|
+
filename: f.filename
|
|
13027
|
+
};
|
|
13028
|
+
})
|
|
13029
|
+
);
|
|
13030
|
+
userContent = [
|
|
13031
|
+
{ type: "text", text: opts.task },
|
|
13032
|
+
...uploadedParts
|
|
13033
|
+
];
|
|
13034
|
+
}
|
|
13035
|
+
const turnTimestamp = Date.now();
|
|
13036
|
+
const userMessage = {
|
|
13037
|
+
role: "user",
|
|
13038
|
+
content: userContent,
|
|
13039
|
+
metadata: { id: randomUUID6(), timestamp: turnTimestamp }
|
|
13040
|
+
};
|
|
13041
|
+
const assistantId = randomUUID6();
|
|
13042
|
+
const draft = createTurnDraftState();
|
|
13043
|
+
let latestRunId = conversation.runtimeRunId ?? "";
|
|
13044
|
+
let runCancelled = false;
|
|
13045
|
+
let runContinuationMessages;
|
|
13046
|
+
let cancelHarnessMessages;
|
|
13047
|
+
let checkpointedRun = false;
|
|
13048
|
+
const buildMessages = () => {
|
|
13049
|
+
const draftSections = cloneSections(draft.sections);
|
|
13050
|
+
if (draft.currentTools.length > 0) {
|
|
13051
|
+
draftSections.push({ type: "tools", content: [...draft.currentTools] });
|
|
13052
|
+
}
|
|
13053
|
+
if (draft.currentText.length > 0) {
|
|
13054
|
+
draftSections.push({ type: "text", content: draft.currentText });
|
|
13055
|
+
}
|
|
13056
|
+
const userTurn = [userMessage];
|
|
13057
|
+
const hasDraftContent = draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
|
|
13058
|
+
if (!hasDraftContent) {
|
|
13059
|
+
return [...historyMessages, ...userTurn];
|
|
13060
|
+
}
|
|
13061
|
+
return [
|
|
13062
|
+
...historyMessages,
|
|
13063
|
+
...userTurn,
|
|
13064
|
+
{
|
|
13065
|
+
role: "assistant",
|
|
13066
|
+
content: draft.assistantResponse,
|
|
13067
|
+
metadata: buildAssistantMetadata(draft, draftSections, {
|
|
13068
|
+
id: assistantId,
|
|
13069
|
+
timestamp: turnTimestamp
|
|
13070
|
+
})
|
|
13071
|
+
}
|
|
13072
|
+
];
|
|
13073
|
+
};
|
|
13074
|
+
const persistDraft = async () => {
|
|
13075
|
+
if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
|
|
13076
|
+
conversation.messages = buildMessages();
|
|
13077
|
+
conversation.updatedAt = Date.now();
|
|
13078
|
+
await opts.conversationStore.update(conversation);
|
|
13079
|
+
};
|
|
13080
|
+
conversation.messages = [...historyMessages, userMessage];
|
|
13081
|
+
conversation.subagentCallbackCount = 0;
|
|
13082
|
+
conversation._continuationCount = void 0;
|
|
13083
|
+
conversation.updatedAt = Date.now();
|
|
13084
|
+
opts.conversationStore.update(conversation).catch((err) => {
|
|
13085
|
+
log.error(
|
|
13086
|
+
`failed to persist user turn: ${err instanceof Error ? err.message : String(err)}`
|
|
13087
|
+
);
|
|
13088
|
+
});
|
|
13089
|
+
try {
|
|
13090
|
+
const execution = await executeConversationTurn({
|
|
13091
|
+
harness: opts.harness,
|
|
13092
|
+
runInput: {
|
|
13093
|
+
task: opts.task,
|
|
13094
|
+
conversationId: opts.conversationId,
|
|
13095
|
+
tenantId: opts.tenantId ?? void 0,
|
|
13096
|
+
parameters: withToolResultArchiveParam(
|
|
13097
|
+
{
|
|
13098
|
+
...opts.parameters ?? {},
|
|
13099
|
+
__activeConversationId: opts.conversationId,
|
|
13100
|
+
__ownerId: conversation.ownerId
|
|
13101
|
+
},
|
|
13102
|
+
conversation
|
|
13103
|
+
),
|
|
13104
|
+
messages: harnessMessages,
|
|
13105
|
+
files: opts.files && opts.files.length > 0 ? opts.files : void 0,
|
|
13106
|
+
abortSignal: opts.abortSignal
|
|
13107
|
+
},
|
|
13108
|
+
initialContextTokens: conversation.contextTokens ?? 0,
|
|
13109
|
+
initialContextWindow: conversation.contextWindow ?? 0,
|
|
13110
|
+
onEvent: async (event, eventDraft) => {
|
|
13111
|
+
draft.assistantResponse = eventDraft.assistantResponse;
|
|
13112
|
+
draft.toolTimeline = eventDraft.toolTimeline;
|
|
13113
|
+
draft.sections = eventDraft.sections;
|
|
13114
|
+
draft.currentTools = eventDraft.currentTools;
|
|
13115
|
+
draft.currentText = eventDraft.currentText;
|
|
13116
|
+
if (event.type === "run:started") {
|
|
13117
|
+
latestRunId = event.runId;
|
|
13118
|
+
}
|
|
13119
|
+
if (event.type === "run:cancelled") {
|
|
13120
|
+
runCancelled = true;
|
|
13121
|
+
if (event.messages) cancelHarnessMessages = event.messages;
|
|
13122
|
+
}
|
|
13123
|
+
if (event.type === "compaction:completed") {
|
|
13124
|
+
if (event.compactedMessages) {
|
|
13125
|
+
historyMessages.length = 0;
|
|
13126
|
+
historyMessages.push(...event.compactedMessages);
|
|
13127
|
+
const preservedFromHistory = historyMessages.length - 1;
|
|
13128
|
+
const removedCount = preRunMessages.length - Math.max(0, preservedFromHistory);
|
|
13129
|
+
const existingHistory = conversation.compactedHistory ?? [];
|
|
13130
|
+
conversation.compactedHistory = [
|
|
13131
|
+
...existingHistory,
|
|
13132
|
+
...preRunMessages.slice(0, removedCount)
|
|
13133
|
+
];
|
|
13134
|
+
}
|
|
13135
|
+
}
|
|
13136
|
+
if (event.type === "step:completed") {
|
|
13137
|
+
await persistDraft();
|
|
13138
|
+
}
|
|
13139
|
+
if (event.type === "tool:approval:required") {
|
|
13140
|
+
const toolText = `- approval required \`${event.tool}\``;
|
|
13141
|
+
draft.toolTimeline.push(toolText);
|
|
13142
|
+
draft.currentTools.push(toolText);
|
|
13143
|
+
const existing = Array.isArray(conversation.pendingApprovals) ? conversation.pendingApprovals : [];
|
|
13144
|
+
if (!existing.some((a) => a.approvalId === event.approvalId)) {
|
|
13145
|
+
conversation.pendingApprovals = [
|
|
13146
|
+
...existing,
|
|
13147
|
+
{
|
|
13148
|
+
approvalId: event.approvalId,
|
|
13149
|
+
runId: latestRunId || conversation.runtimeRunId || "",
|
|
13150
|
+
tool: event.tool,
|
|
13151
|
+
toolCallId: void 0,
|
|
13152
|
+
input: event.input ?? {},
|
|
13153
|
+
checkpointMessages: void 0,
|
|
13154
|
+
baseMessageCount: historyMessages.length,
|
|
13155
|
+
pendingToolCalls: []
|
|
13156
|
+
}
|
|
13157
|
+
];
|
|
13158
|
+
conversation.updatedAt = Date.now();
|
|
13159
|
+
await opts.conversationStore.update(conversation);
|
|
13160
|
+
}
|
|
13161
|
+
await persistDraft();
|
|
13162
|
+
}
|
|
13163
|
+
if (event.type === "tool:approval:checkpoint") {
|
|
13164
|
+
conversation.messages = buildMessages();
|
|
13165
|
+
conversation.pendingApprovals = buildApprovalCheckpoints({
|
|
13166
|
+
approvals: event.approvals,
|
|
13167
|
+
runId: latestRunId,
|
|
13168
|
+
checkpointMessages: event.checkpointMessages,
|
|
13169
|
+
baseMessageCount: historyMessages.length,
|
|
13170
|
+
pendingToolCalls: event.pendingToolCalls
|
|
13171
|
+
});
|
|
13172
|
+
conversation._toolResultArchive = opts.harness.getToolResultArchive(
|
|
13173
|
+
opts.conversationId
|
|
13174
|
+
);
|
|
13175
|
+
conversation.updatedAt = Date.now();
|
|
13176
|
+
await opts.conversationStore.update(conversation);
|
|
13177
|
+
checkpointedRun = true;
|
|
13178
|
+
}
|
|
13179
|
+
if (event.type === "run:completed") {
|
|
13180
|
+
if (event.result.continuation && event.result.continuationMessages) {
|
|
13181
|
+
runContinuationMessages = event.result.continuationMessages;
|
|
13182
|
+
conversation.messages = buildMessages();
|
|
13183
|
+
conversation._continuationMessages = runContinuationMessages;
|
|
13184
|
+
conversation._harnessMessages = runContinuationMessages;
|
|
13185
|
+
conversation._toolResultArchive = opts.harness.getToolResultArchive(
|
|
13186
|
+
opts.conversationId
|
|
13187
|
+
);
|
|
13188
|
+
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
13189
|
+
if (!checkpointedRun) {
|
|
13190
|
+
conversation.pendingApprovals = [];
|
|
13191
|
+
}
|
|
13192
|
+
if ((event.result.contextTokens ?? 0) > 0) {
|
|
13193
|
+
conversation.contextTokens = event.result.contextTokens;
|
|
13194
|
+
}
|
|
13195
|
+
if ((event.result.contextWindow ?? 0) > 0) {
|
|
13196
|
+
conversation.contextWindow = event.result.contextWindow;
|
|
13197
|
+
}
|
|
13198
|
+
conversation.updatedAt = Date.now();
|
|
13199
|
+
await opts.conversationStore.update(conversation);
|
|
13200
|
+
}
|
|
13201
|
+
}
|
|
13202
|
+
if (opts.onEvent) {
|
|
13203
|
+
await opts.onEvent(event);
|
|
13204
|
+
}
|
|
13205
|
+
}
|
|
13206
|
+
});
|
|
13207
|
+
flushTurnDraft(draft);
|
|
13208
|
+
latestRunId = execution.latestRunId || latestRunId;
|
|
13209
|
+
if (!checkpointedRun && !runContinuationMessages) {
|
|
13210
|
+
conversation.messages = buildMessages();
|
|
13211
|
+
applyTurnMetadata(
|
|
13212
|
+
conversation,
|
|
13213
|
+
{
|
|
13214
|
+
latestRunId,
|
|
13215
|
+
contextTokens: execution.runContextTokens,
|
|
13216
|
+
contextWindow: execution.runContextWindow,
|
|
13217
|
+
harnessMessages: execution.runHarnessMessages,
|
|
13218
|
+
toolResultArchive: opts.harness.getToolResultArchive(opts.conversationId)
|
|
13219
|
+
},
|
|
13220
|
+
{ shouldRebuildCanonical }
|
|
13221
|
+
);
|
|
13222
|
+
await opts.conversationStore.update(conversation);
|
|
13223
|
+
}
|
|
13224
|
+
return {
|
|
13225
|
+
latestRunId,
|
|
13226
|
+
cancelled: runCancelled,
|
|
13227
|
+
errored: false,
|
|
13228
|
+
continuation: !!runContinuationMessages,
|
|
13229
|
+
checkpointed: checkpointedRun,
|
|
13230
|
+
contextTokens: execution.runContextTokens,
|
|
13231
|
+
contextWindow: execution.runContextWindow
|
|
13232
|
+
};
|
|
13233
|
+
} catch (error) {
|
|
13234
|
+
flushTurnDraft(draft);
|
|
13235
|
+
const aborted = opts.abortSignal?.aborted === true;
|
|
13236
|
+
if (aborted || runCancelled) {
|
|
13237
|
+
if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
|
|
13238
|
+
conversation.messages = buildMessages();
|
|
13239
|
+
applyTurnMetadata(
|
|
13240
|
+
conversation,
|
|
13241
|
+
{
|
|
13242
|
+
latestRunId,
|
|
13243
|
+
contextTokens: 0,
|
|
13244
|
+
contextWindow: 0,
|
|
13245
|
+
harnessMessages: cancelHarnessMessages,
|
|
13246
|
+
toolResultArchive: opts.harness.getToolResultArchive(opts.conversationId)
|
|
13247
|
+
},
|
|
13248
|
+
{ shouldRebuildCanonical: true }
|
|
13249
|
+
);
|
|
13250
|
+
await opts.conversationStore.update(conversation);
|
|
13251
|
+
}
|
|
13252
|
+
if (!checkpointedRun) {
|
|
13253
|
+
const fresh = await opts.conversationStore.get(opts.conversationId);
|
|
13254
|
+
if (fresh && Array.isArray(fresh.pendingApprovals) && fresh.pendingApprovals.length > 0) {
|
|
13255
|
+
fresh.pendingApprovals = [];
|
|
13256
|
+
await opts.conversationStore.update(fresh);
|
|
13257
|
+
}
|
|
13258
|
+
}
|
|
13259
|
+
return {
|
|
13260
|
+
latestRunId,
|
|
13261
|
+
cancelled: true,
|
|
13262
|
+
errored: false,
|
|
13263
|
+
continuation: false,
|
|
13264
|
+
checkpointed: checkpointedRun,
|
|
13265
|
+
contextTokens: 0,
|
|
13266
|
+
contextWindow: 0
|
|
13267
|
+
};
|
|
13268
|
+
}
|
|
13269
|
+
const errorEvent = {
|
|
13270
|
+
type: "run:error",
|
|
13271
|
+
runId: latestRunId || "run_unknown",
|
|
13272
|
+
error: {
|
|
13273
|
+
code: "RUN_ERROR",
|
|
13274
|
+
message: error instanceof Error ? error.message : "Unknown error"
|
|
13275
|
+
}
|
|
13276
|
+
};
|
|
13277
|
+
if (opts.onEvent) {
|
|
13278
|
+
try {
|
|
13279
|
+
await opts.onEvent(errorEvent);
|
|
13280
|
+
} catch (hookErr) {
|
|
13281
|
+
log.error(
|
|
13282
|
+
`onEvent threw on run:error: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`
|
|
13283
|
+
);
|
|
13284
|
+
}
|
|
13285
|
+
}
|
|
13286
|
+
if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
|
|
13287
|
+
conversation.messages = buildMessages();
|
|
13288
|
+
conversation.updatedAt = Date.now();
|
|
13289
|
+
await opts.conversationStore.update(conversation);
|
|
13290
|
+
}
|
|
13291
|
+
return {
|
|
13292
|
+
latestRunId,
|
|
13293
|
+
cancelled: false,
|
|
13294
|
+
errored: true,
|
|
13295
|
+
continuation: false,
|
|
13296
|
+
checkpointed: checkpointedRun,
|
|
13297
|
+
contextTokens: 0,
|
|
13298
|
+
contextWindow: 0
|
|
13299
|
+
};
|
|
13300
|
+
}
|
|
13301
|
+
};
|
|
13302
|
+
|
|
12994
13303
|
// src/index.ts
|
|
12995
13304
|
import { defineTool as defineTool13 } from "@poncho-ai/sdk";
|
|
12996
13305
|
export {
|
|
@@ -13098,6 +13407,7 @@ export {
|
|
|
13098
13407
|
resolveRunRequest,
|
|
13099
13408
|
resolveSkillDirs,
|
|
13100
13409
|
resolveStateConfig,
|
|
13410
|
+
runConversationTurn,
|
|
13101
13411
|
slugifyStorageComponent,
|
|
13102
13412
|
startOpenAICodexDeviceAuth,
|
|
13103
13413
|
verifyTenantToken,
|
package/package.json
CHANGED
package/src/harness.ts
CHANGED
|
@@ -1940,6 +1940,13 @@ export class AgentHarness {
|
|
|
1940
1940
|
return this.loadedSkills.map((s) => ({ name: s.name, description: s.description }));
|
|
1941
1941
|
}
|
|
1942
1942
|
|
|
1943
|
+
async listSkillsForTenant(
|
|
1944
|
+
tenantId: string | undefined | null,
|
|
1945
|
+
): Promise<Array<{ name: string; description: string }>> {
|
|
1946
|
+
const skills = await this.getSkillsForTenant(tenantId);
|
|
1947
|
+
return skills.map((s) => ({ name: s.name, description: s.description }));
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1943
1950
|
/**
|
|
1944
1951
|
* Wraps the run() generator with an OTel root span (invoke_agent) so all
|
|
1945
1952
|
* child spans (LLM calls via AI SDK, tool execution) group under one trace.
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// runConversationTurn — load-bearing helper that runs a single primary chat
|
|
3
|
+
// turn end-to-end against a ConversationStore: loads the conversation,
|
|
4
|
+
// persists the user message before the run, drives the model + tool loop
|
|
5
|
+
// via executeConversationTurn, periodically persists the in-flight assistant
|
|
6
|
+
// draft, handles approval checkpoints + continuations + cancellation, and
|
|
7
|
+
// finalises the conversation row on completion.
|
|
8
|
+
//
|
|
9
|
+
// This was extracted from packages/cli/src/index.ts (POST
|
|
10
|
+
// /api/conversations/:id/messages handler) so consumers other than the CLI
|
|
11
|
+
// (PonchOS, custom servers) can ship the *same* conversation lifecycle
|
|
12
|
+
// without copy-pasting hundreds of lines.
|
|
13
|
+
//
|
|
14
|
+
// Caller responsibilities (NOT done here):
|
|
15
|
+
// - auth / ownership checks
|
|
16
|
+
// - active-run dedup (one run at a time per conversation)
|
|
17
|
+
// - streaming events to a real client (use opts.onEvent)
|
|
18
|
+
// - triggering continuation runs after this returns continuation: true
|
|
19
|
+
// - conversation title inference (helper preserves existing title)
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
import { randomUUID } from "node:crypto";
|
|
23
|
+
import type { AgentEvent, FileInput, Message } from "@poncho-ai/sdk";
|
|
24
|
+
import { createLogger } from "@poncho-ai/sdk";
|
|
25
|
+
import type { AgentHarness } from "../harness.js";
|
|
26
|
+
import type { ConversationStore } from "../state.js";
|
|
27
|
+
import { deriveUploadKey } from "../upload-store.js";
|
|
28
|
+
import { withToolResultArchiveParam } from "./continuation.js";
|
|
29
|
+
import { resolveRunRequest } from "./history.js";
|
|
30
|
+
import {
|
|
31
|
+
applyTurnMetadata,
|
|
32
|
+
buildApprovalCheckpoints,
|
|
33
|
+
buildAssistantMetadata,
|
|
34
|
+
cloneSections,
|
|
35
|
+
createTurnDraftState,
|
|
36
|
+
executeConversationTurn,
|
|
37
|
+
flushTurnDraft,
|
|
38
|
+
} from "./turn.js";
|
|
39
|
+
|
|
40
|
+
const log = createLogger("orchestrator");
|
|
41
|
+
|
|
42
|
+
export interface RunConversationTurnOpts {
|
|
43
|
+
/** Initialised harness instance. */
|
|
44
|
+
harness: AgentHarness;
|
|
45
|
+
/** Conversation store backing the turn (typically `engine.conversations` from a StorageEngine). */
|
|
46
|
+
conversationStore: ConversationStore;
|
|
47
|
+
conversationId: string;
|
|
48
|
+
/** The user's new message text. Required (use `""` if you only want to attach files). */
|
|
49
|
+
task: string;
|
|
50
|
+
/**
|
|
51
|
+
* Optional file attachments (FileInput.data is base64 / data URI / https URL).
|
|
52
|
+
* Files are uploaded via `harness.uploadStore` first so the persisted user
|
|
53
|
+
* message references stable URLs instead of fat base64 blobs.
|
|
54
|
+
*/
|
|
55
|
+
files?: FileInput[];
|
|
56
|
+
/**
|
|
57
|
+
* Extra parameters merged into runInput.parameters. Use this for recall
|
|
58
|
+
* corpus, archive lookup keys, messaging metadata, etc. Do NOT include
|
|
59
|
+
* `__activeConversationId`, `__ownerId`, or the tool-result-archive — the
|
|
60
|
+
* helper sets those itself.
|
|
61
|
+
*/
|
|
62
|
+
parameters?: Record<string, unknown>;
|
|
63
|
+
abortSignal?: AbortSignal;
|
|
64
|
+
tenantId?: string | null;
|
|
65
|
+
/** Per-event hook — called for every AgentEvent yielded by the run, in order. */
|
|
66
|
+
onEvent?: (event: AgentEvent) => void | Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface RunConversationTurnResult {
|
|
70
|
+
/** runId of the most recent run started during this turn. */
|
|
71
|
+
latestRunId: string;
|
|
72
|
+
/** True if the run was cancelled (via abortSignal or run:cancelled event). */
|
|
73
|
+
cancelled: boolean;
|
|
74
|
+
/** True if the run errored. The error has been emitted via onEvent as run:error. */
|
|
75
|
+
errored: boolean;
|
|
76
|
+
/** True if the run requested a continuation. Caller is responsible for triggering the continuation. */
|
|
77
|
+
continuation: boolean;
|
|
78
|
+
/** True if the run paused at a tool-approval checkpoint. */
|
|
79
|
+
checkpointed: boolean;
|
|
80
|
+
contextTokens: number;
|
|
81
|
+
contextWindow: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const runConversationTurn = async (
|
|
85
|
+
opts: RunConversationTurnOpts,
|
|
86
|
+
): Promise<RunConversationTurnResult> => {
|
|
87
|
+
const conversation = await opts.conversationStore.getWithArchive(opts.conversationId);
|
|
88
|
+
if (!conversation) {
|
|
89
|
+
throw new Error(`Conversation not found: ${opts.conversationId}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const canonicalHistory = resolveRunRequest(conversation, {
|
|
93
|
+
conversationId: opts.conversationId,
|
|
94
|
+
messages: conversation.messages,
|
|
95
|
+
});
|
|
96
|
+
const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
|
|
97
|
+
const harnessMessages = [...canonicalHistory.messages];
|
|
98
|
+
const historyMessages = [...conversation.messages];
|
|
99
|
+
const preRunMessages = [...conversation.messages];
|
|
100
|
+
|
|
101
|
+
// Build user content — upload any files first so the persisted message
|
|
102
|
+
// carries stable refs instead of fat base64 blobs.
|
|
103
|
+
let userContent: Message["content"] = opts.task;
|
|
104
|
+
if (opts.files && opts.files.length > 0 && opts.harness.uploadStore) {
|
|
105
|
+
const uploadedParts = await Promise.all(
|
|
106
|
+
opts.files.map(async (f) => {
|
|
107
|
+
const buf = Buffer.from(f.data, "base64");
|
|
108
|
+
const key = deriveUploadKey(buf, f.mediaType);
|
|
109
|
+
const ref = await opts.harness.uploadStore!.put(key, buf, f.mediaType);
|
|
110
|
+
return {
|
|
111
|
+
type: "file" as const,
|
|
112
|
+
data: ref,
|
|
113
|
+
mediaType: f.mediaType,
|
|
114
|
+
filename: f.filename,
|
|
115
|
+
};
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
userContent = [
|
|
119
|
+
{ type: "text" as const, text: opts.task },
|
|
120
|
+
...uploadedParts,
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const turnTimestamp = Date.now();
|
|
125
|
+
const userMessage: Message = {
|
|
126
|
+
role: "user",
|
|
127
|
+
content: userContent,
|
|
128
|
+
metadata: { id: randomUUID(), timestamp: turnTimestamp },
|
|
129
|
+
};
|
|
130
|
+
const assistantId = randomUUID();
|
|
131
|
+
const draft = createTurnDraftState();
|
|
132
|
+
|
|
133
|
+
let latestRunId = conversation.runtimeRunId ?? "";
|
|
134
|
+
let runCancelled = false;
|
|
135
|
+
let runContinuationMessages: Message[] | undefined;
|
|
136
|
+
let cancelHarnessMessages: Message[] | undefined;
|
|
137
|
+
let checkpointedRun = false;
|
|
138
|
+
|
|
139
|
+
const buildMessages = (): Message[] => {
|
|
140
|
+
const draftSections = cloneSections(draft.sections);
|
|
141
|
+
if (draft.currentTools.length > 0) {
|
|
142
|
+
draftSections.push({ type: "tools", content: [...draft.currentTools] });
|
|
143
|
+
}
|
|
144
|
+
if (draft.currentText.length > 0) {
|
|
145
|
+
draftSections.push({ type: "text", content: draft.currentText });
|
|
146
|
+
}
|
|
147
|
+
const userTurn: Message[] = [userMessage];
|
|
148
|
+
const hasDraftContent =
|
|
149
|
+
draft.assistantResponse.length > 0 ||
|
|
150
|
+
draft.toolTimeline.length > 0 ||
|
|
151
|
+
draftSections.length > 0;
|
|
152
|
+
if (!hasDraftContent) {
|
|
153
|
+
return [...historyMessages, ...userTurn];
|
|
154
|
+
}
|
|
155
|
+
return [
|
|
156
|
+
...historyMessages,
|
|
157
|
+
...userTurn,
|
|
158
|
+
{
|
|
159
|
+
role: "assistant" as const,
|
|
160
|
+
content: draft.assistantResponse,
|
|
161
|
+
metadata: buildAssistantMetadata(draft, draftSections, {
|
|
162
|
+
id: assistantId,
|
|
163
|
+
timestamp: turnTimestamp,
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const persistDraft = async (): Promise<void> => {
|
|
170
|
+
if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
|
|
171
|
+
conversation.messages = buildMessages();
|
|
172
|
+
conversation.updatedAt = Date.now();
|
|
173
|
+
await opts.conversationStore.update(conversation);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Persist the user turn immediately so a crash mid-run still records what
|
|
177
|
+
// the user said. Fire-and-forget — don't block the run.
|
|
178
|
+
conversation.messages = [...historyMessages, userMessage];
|
|
179
|
+
conversation.subagentCallbackCount = 0;
|
|
180
|
+
conversation._continuationCount = undefined;
|
|
181
|
+
conversation.updatedAt = Date.now();
|
|
182
|
+
opts.conversationStore.update(conversation).catch((err) => {
|
|
183
|
+
log.error(
|
|
184
|
+
`failed to persist user turn: ${err instanceof Error ? err.message : String(err)}`,
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const execution = await executeConversationTurn({
|
|
190
|
+
harness: opts.harness,
|
|
191
|
+
runInput: {
|
|
192
|
+
task: opts.task,
|
|
193
|
+
conversationId: opts.conversationId,
|
|
194
|
+
tenantId: opts.tenantId ?? undefined,
|
|
195
|
+
parameters: withToolResultArchiveParam(
|
|
196
|
+
{
|
|
197
|
+
...(opts.parameters ?? {}),
|
|
198
|
+
__activeConversationId: opts.conversationId,
|
|
199
|
+
__ownerId: conversation.ownerId,
|
|
200
|
+
},
|
|
201
|
+
conversation,
|
|
202
|
+
),
|
|
203
|
+
messages: harnessMessages,
|
|
204
|
+
files: opts.files && opts.files.length > 0 ? opts.files : undefined,
|
|
205
|
+
abortSignal: opts.abortSignal,
|
|
206
|
+
},
|
|
207
|
+
initialContextTokens: conversation.contextTokens ?? 0,
|
|
208
|
+
initialContextWindow: conversation.contextWindow ?? 0,
|
|
209
|
+
onEvent: async (event, eventDraft) => {
|
|
210
|
+
// Sync our outer draft from the executor's so persistDraft sees the latest state.
|
|
211
|
+
draft.assistantResponse = eventDraft.assistantResponse;
|
|
212
|
+
draft.toolTimeline = eventDraft.toolTimeline;
|
|
213
|
+
draft.sections = eventDraft.sections;
|
|
214
|
+
draft.currentTools = eventDraft.currentTools;
|
|
215
|
+
draft.currentText = eventDraft.currentText;
|
|
216
|
+
|
|
217
|
+
if (event.type === "run:started") {
|
|
218
|
+
latestRunId = event.runId;
|
|
219
|
+
}
|
|
220
|
+
if (event.type === "run:cancelled") {
|
|
221
|
+
runCancelled = true;
|
|
222
|
+
if (event.messages) cancelHarnessMessages = event.messages;
|
|
223
|
+
}
|
|
224
|
+
if (event.type === "compaction:completed") {
|
|
225
|
+
if (event.compactedMessages) {
|
|
226
|
+
historyMessages.length = 0;
|
|
227
|
+
historyMessages.push(...event.compactedMessages);
|
|
228
|
+
const preservedFromHistory = historyMessages.length - 1;
|
|
229
|
+
const removedCount =
|
|
230
|
+
preRunMessages.length - Math.max(0, preservedFromHistory);
|
|
231
|
+
const existingHistory = conversation.compactedHistory ?? [];
|
|
232
|
+
conversation.compactedHistory = [
|
|
233
|
+
...existingHistory,
|
|
234
|
+
...preRunMessages.slice(0, removedCount),
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (event.type === "step:completed") {
|
|
239
|
+
await persistDraft();
|
|
240
|
+
}
|
|
241
|
+
if (event.type === "tool:approval:required") {
|
|
242
|
+
const toolText = `- approval required \`${event.tool}\``;
|
|
243
|
+
draft.toolTimeline.push(toolText);
|
|
244
|
+
draft.currentTools.push(toolText);
|
|
245
|
+
const existing = Array.isArray(conversation.pendingApprovals)
|
|
246
|
+
? conversation.pendingApprovals
|
|
247
|
+
: [];
|
|
248
|
+
if (!existing.some((a) => a.approvalId === event.approvalId)) {
|
|
249
|
+
conversation.pendingApprovals = [
|
|
250
|
+
...existing,
|
|
251
|
+
{
|
|
252
|
+
approvalId: event.approvalId,
|
|
253
|
+
runId: latestRunId || conversation.runtimeRunId || "",
|
|
254
|
+
tool: event.tool,
|
|
255
|
+
toolCallId: undefined,
|
|
256
|
+
input: (event.input ?? {}) as Record<string, unknown>,
|
|
257
|
+
checkpointMessages: undefined,
|
|
258
|
+
baseMessageCount: historyMessages.length,
|
|
259
|
+
pendingToolCalls: [],
|
|
260
|
+
},
|
|
261
|
+
];
|
|
262
|
+
conversation.updatedAt = Date.now();
|
|
263
|
+
await opts.conversationStore.update(conversation);
|
|
264
|
+
}
|
|
265
|
+
await persistDraft();
|
|
266
|
+
}
|
|
267
|
+
if (event.type === "tool:approval:checkpoint") {
|
|
268
|
+
conversation.messages = buildMessages();
|
|
269
|
+
conversation.pendingApprovals = buildApprovalCheckpoints({
|
|
270
|
+
approvals: event.approvals,
|
|
271
|
+
runId: latestRunId,
|
|
272
|
+
checkpointMessages: event.checkpointMessages,
|
|
273
|
+
baseMessageCount: historyMessages.length,
|
|
274
|
+
pendingToolCalls: event.pendingToolCalls,
|
|
275
|
+
});
|
|
276
|
+
conversation._toolResultArchive = opts.harness.getToolResultArchive(
|
|
277
|
+
opts.conversationId,
|
|
278
|
+
);
|
|
279
|
+
conversation.updatedAt = Date.now();
|
|
280
|
+
await opts.conversationStore.update(conversation);
|
|
281
|
+
checkpointedRun = true;
|
|
282
|
+
}
|
|
283
|
+
if (event.type === "run:completed") {
|
|
284
|
+
if (event.result.continuation && event.result.continuationMessages) {
|
|
285
|
+
runContinuationMessages = event.result.continuationMessages;
|
|
286
|
+
conversation.messages = buildMessages();
|
|
287
|
+
conversation._continuationMessages = runContinuationMessages;
|
|
288
|
+
conversation._harnessMessages = runContinuationMessages;
|
|
289
|
+
conversation._toolResultArchive = opts.harness.getToolResultArchive(
|
|
290
|
+
opts.conversationId,
|
|
291
|
+
);
|
|
292
|
+
conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
|
|
293
|
+
if (!checkpointedRun) {
|
|
294
|
+
conversation.pendingApprovals = [];
|
|
295
|
+
}
|
|
296
|
+
if ((event.result.contextTokens ?? 0) > 0) {
|
|
297
|
+
conversation.contextTokens = event.result.contextTokens!;
|
|
298
|
+
}
|
|
299
|
+
if ((event.result.contextWindow ?? 0) > 0) {
|
|
300
|
+
conversation.contextWindow = event.result.contextWindow!;
|
|
301
|
+
}
|
|
302
|
+
conversation.updatedAt = Date.now();
|
|
303
|
+
await opts.conversationStore.update(conversation);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (opts.onEvent) {
|
|
308
|
+
await opts.onEvent(event);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
flushTurnDraft(draft);
|
|
314
|
+
latestRunId = execution.latestRunId || latestRunId;
|
|
315
|
+
|
|
316
|
+
if (!checkpointedRun && !runContinuationMessages) {
|
|
317
|
+
conversation.messages = buildMessages();
|
|
318
|
+
applyTurnMetadata(
|
|
319
|
+
conversation,
|
|
320
|
+
{
|
|
321
|
+
latestRunId,
|
|
322
|
+
contextTokens: execution.runContextTokens,
|
|
323
|
+
contextWindow: execution.runContextWindow,
|
|
324
|
+
harnessMessages: execution.runHarnessMessages,
|
|
325
|
+
toolResultArchive: opts.harness.getToolResultArchive(opts.conversationId),
|
|
326
|
+
},
|
|
327
|
+
{ shouldRebuildCanonical },
|
|
328
|
+
);
|
|
329
|
+
await opts.conversationStore.update(conversation);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
latestRunId,
|
|
334
|
+
cancelled: runCancelled,
|
|
335
|
+
errored: false,
|
|
336
|
+
continuation: !!runContinuationMessages,
|
|
337
|
+
checkpointed: checkpointedRun,
|
|
338
|
+
contextTokens: execution.runContextTokens,
|
|
339
|
+
contextWindow: execution.runContextWindow,
|
|
340
|
+
};
|
|
341
|
+
} catch (error) {
|
|
342
|
+
flushTurnDraft(draft);
|
|
343
|
+
const aborted = opts.abortSignal?.aborted === true;
|
|
344
|
+
if (aborted || runCancelled) {
|
|
345
|
+
if (
|
|
346
|
+
draft.assistantResponse.length > 0 ||
|
|
347
|
+
draft.toolTimeline.length > 0 ||
|
|
348
|
+
draft.sections.length > 0
|
|
349
|
+
) {
|
|
350
|
+
conversation.messages = buildMessages();
|
|
351
|
+
applyTurnMetadata(
|
|
352
|
+
conversation,
|
|
353
|
+
{
|
|
354
|
+
latestRunId,
|
|
355
|
+
contextTokens: 0,
|
|
356
|
+
contextWindow: 0,
|
|
357
|
+
harnessMessages: cancelHarnessMessages,
|
|
358
|
+
toolResultArchive: opts.harness.getToolResultArchive(opts.conversationId),
|
|
359
|
+
},
|
|
360
|
+
{ shouldRebuildCanonical: true },
|
|
361
|
+
);
|
|
362
|
+
await opts.conversationStore.update(conversation);
|
|
363
|
+
}
|
|
364
|
+
if (!checkpointedRun) {
|
|
365
|
+
// Clear any pending approvals — the run was cancelled, they're stale.
|
|
366
|
+
const fresh = await opts.conversationStore.get(opts.conversationId);
|
|
367
|
+
if (fresh && Array.isArray(fresh.pendingApprovals) && fresh.pendingApprovals.length > 0) {
|
|
368
|
+
fresh.pendingApprovals = [];
|
|
369
|
+
await opts.conversationStore.update(fresh);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
latestRunId,
|
|
374
|
+
cancelled: true,
|
|
375
|
+
errored: false,
|
|
376
|
+
continuation: false,
|
|
377
|
+
checkpointed: checkpointedRun,
|
|
378
|
+
contextTokens: 0,
|
|
379
|
+
contextWindow: 0,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Real error: emit run:error, persist whatever we have.
|
|
384
|
+
const errorEvent: AgentEvent = {
|
|
385
|
+
type: "run:error",
|
|
386
|
+
runId: latestRunId || "run_unknown",
|
|
387
|
+
error: {
|
|
388
|
+
code: "RUN_ERROR",
|
|
389
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
if (opts.onEvent) {
|
|
393
|
+
try {
|
|
394
|
+
await opts.onEvent(errorEvent);
|
|
395
|
+
} catch (hookErr) {
|
|
396
|
+
log.error(
|
|
397
|
+
`onEvent threw on run:error: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (
|
|
402
|
+
draft.assistantResponse.length > 0 ||
|
|
403
|
+
draft.toolTimeline.length > 0 ||
|
|
404
|
+
draft.sections.length > 0
|
|
405
|
+
) {
|
|
406
|
+
conversation.messages = buildMessages();
|
|
407
|
+
conversation.updatedAt = Date.now();
|
|
408
|
+
await opts.conversationStore.update(conversation);
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
latestRunId,
|
|
412
|
+
cancelled: false,
|
|
413
|
+
errored: true,
|
|
414
|
+
continuation: false,
|
|
415
|
+
checkpointed: checkpointedRun,
|
|
416
|
+
contextTokens: 0,
|
|
417
|
+
contextWindow: 0,
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
};
|