@poncho-ai/harness 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.41.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.43.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
4
4
 
5
5
  [embed-docs] Generated poncho-docs.ts with 4 topics
@@ -8,9 +8,9 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 493.42 KB
12
11
  ESM dist/isolate-VY35DGLM.js 49.43 KB
13
- ESM ⚡️ Build success in 238ms
12
+ ESM dist/index.js 506.40 KB
13
+ ESM ⚡️ Build success in 222ms
14
14
  DTS Build start
15
- DTS ⚡️ Build success in 7385ms
16
- DTS dist/index.d.ts 77.00 KB
15
+ DTS ⚡️ Build success in 6907ms
16
+ DTS dist/index.d.ts 80.85 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.43.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`ff89631`](https://github.com/cesr/poncho-ai/commit/ff89631715e54d6fdce174943e6e0fc9e4ce5d1e) Thanks [@cesr](https://github.com/cesr)! - harness: export `defaultAgentDefinition` so SDK consumers can match `poncho init` exactly
8
+
9
+ Lifts the `AGENT_TEMPLATE` markdown body from `@poncho-ai/cli` (where it lived
10
+ inside the `init` scaffolding) into a public helper on `@poncho-ai/harness`.
11
+ SDK consumers (PonchOS, custom servers, anyone calling
12
+ `new AgentHarness({ agentDefinition })` directly) can now do:
13
+
14
+ ```ts
15
+ import { defaultAgentDefinition } from "@poncho-ai/harness";
16
+
17
+ const harness = new AgentHarness({
18
+ agentDefinition: defaultAgentDefinition({
19
+ name: "poncho",
20
+ modelName: "claude-sonnet-4-6",
21
+ }),
22
+ // ... storageEngine, config, etc.
23
+ });
24
+ ```
25
+
26
+ This eliminates hand-copying the template — drift between consumers and
27
+ `poncho init` is no longer possible.
28
+
29
+ The CLI's `AGENT_TEMPLATE` export is preserved as a thin back-compat
30
+ wrapper that delegates to `defaultAgentDefinition`. No behavior change.
31
+
32
+ API additions (harness):
33
+ - `defaultAgentDefinition(opts?: DefaultAgentDefinitionOptions): string`
34
+ - `DefaultAgentDefinitionOptions`
35
+ - `DEFAULT_AGENT_NAME`, `DEFAULT_AGENT_DESCRIPTION`,
36
+ `DEFAULT_MODEL_PROVIDER`, `DEFAULT_MODEL_NAME`, `DEFAULT_TEMPERATURE`,
37
+ `DEFAULT_MAX_STEPS`, `DEFAULT_TIMEOUT` constants
38
+
39
+ ## 0.42.0
40
+
41
+ ### Minor Changes
42
+
43
+ - [`39793b0`](https://github.com/cesr/poncho-ai/commit/39793b0ab11ed26f140af6fc9c0cd3e1b1c83fec) Thanks [@cesr](https://github.com/cesr)! - harness: extract `runConversationTurn` helper; refactor CLI to use it
44
+
45
+ Lifts the inline turn lifecycle from the CLI's
46
+ `POST /api/conversations/:id/messages` handler (~280 lines of orchestration)
47
+ into a new public helper at `@poncho-ai/harness`.
48
+
49
+ The helper handles the full conversation lifecycle for a primary chat
50
+ turn: load the conversation with archive, resolve canonical history,
51
+ upload files via the harness's upload store, build stable user/assistant
52
+ ids, persist the user message immediately, drive `executeConversationTurn`,
53
+ periodically persist the in-flight assistant draft on `step:completed`
54
+ and `tool:approval:required`, persist on `tool:approval:checkpoint` and
55
+ `run:completed` continuation, rebuild history on `compaction:completed`,
56
+ apply turn metadata on success, and persist partial state on
57
+ cancel/error.
58
+
59
+ Caller responsibilities (auth, active-run dedup, streaming, continuation
60
+ HTTP self-fetch, title inference) stay outside the helper — passed in
61
+ via opts or handled around the call. `opts.onEvent` is invoked for every
62
+ `AgentEvent` for downstream forwarding (SSE, WebSocket, telemetry, etc.).
63
+
64
+ The CLI's handler now delegates to `runConversationTurn` (drops from
65
+ ~430 to ~150 lines). Consumers like PonchOS can call the same helper
66
+ to ship the _exact_ same conversation lifecycle without duplicating
67
+ the orchestration.
68
+
69
+ Public API additions:
70
+ - `runConversationTurn(opts): Promise<RunConversationTurnResult>`
71
+ - `RunConversationTurnOpts`
72
+ - `RunConversationTurnResult`
73
+
74
+ No behavior changes. The helper is a verbatim extraction of the CLI's
75
+ prior inline implementation.
76
+
77
+ ### Patch Changes
78
+
79
+ - [`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
80
+
81
+ The `/api/slash-commands` endpoint was returning only repo-loaded skills,
82
+ so tenant-authored skills stored in the VFS (`/skills/<name>/SKILL.md`)
83
+ never appeared in the `/` autocomplete bar even though the agent could
84
+ already see and run them at conversation time.
85
+
86
+ The endpoint now resolves skills per-tenant via a new
87
+ `harness.listSkillsForTenant(tenantId)` and applies the same repo-wins
88
+ collision semantics used elsewhere in the harness.
89
+
3
90
  ## 0.41.0
4
91
 
5
92
  ### 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';
@@ -685,6 +685,43 @@ declare const resolveStateConfig: (config: PonchoConfig | undefined) => StateCon
685
685
  declare const resolveMemoryConfig: (config: PonchoConfig | undefined) => MemoryConfig | undefined;
686
686
  declare const loadPonchoConfig: (workingDir: string) => Promise<PonchoConfig | undefined>;
687
687
 
688
+ interface DefaultAgentDefinitionOptions {
689
+ /** Display name for the agent. Default: "agent". */
690
+ name?: string;
691
+ /**
692
+ * Stable identifier embedded in the frontmatter. Default: a fresh
693
+ * `agent_<32hex>`. Note: when an injected `StorageEngine` is also passed
694
+ * to `AgentHarness`, the engine's `agentId` overrides this at runtime, so
695
+ * SDK consumers can leave it default.
696
+ */
697
+ id?: string;
698
+ /** Frontmatter description. Default: "A helpful Poncho assistant". */
699
+ description?: string;
700
+ /** Model provider. Default: "anthropic". */
701
+ modelProvider?: "anthropic" | "openai" | "openai-codex";
702
+ /** Model name. Default: "claude-opus-4-5". */
703
+ modelName?: string;
704
+ /** Sampling temperature. Default: 0.2. */
705
+ temperature?: number;
706
+ /** Max tool-call steps per run. Default: 20. */
707
+ maxSteps?: number;
708
+ /** Hard timeout in seconds. Default: 300. */
709
+ timeout?: number;
710
+ }
711
+ declare const DEFAULT_AGENT_NAME = "agent";
712
+ declare const DEFAULT_AGENT_DESCRIPTION = "A helpful Poncho assistant";
713
+ declare const DEFAULT_MODEL_PROVIDER: "anthropic";
714
+ declare const DEFAULT_MODEL_NAME = "claude-opus-4-5";
715
+ declare const DEFAULT_TEMPERATURE = 0.2;
716
+ declare const DEFAULT_MAX_STEPS = 20;
717
+ declare const DEFAULT_TIMEOUT = 300;
718
+ /**
719
+ * Returns the canonical default agent definition as a markdown string,
720
+ * ready to pass to `new AgentHarness({ agentDefinition })`. This is the
721
+ * exact same template `poncho init` writes to `AGENT.md`.
722
+ */
723
+ declare const defaultAgentDefinition: (opts?: DefaultAgentDefinitionOptions) => string;
724
+
688
725
  declare const createDefaultTools: (workingDir: string) => ToolDefinition[];
689
726
  declare const createWriteTool: (workingDir: string) => ToolDefinition;
690
727
  declare const createEditTool: (workingDir: string) => ToolDefinition;
@@ -1186,6 +1223,10 @@ declare class AgentHarness {
1186
1223
  name: string;
1187
1224
  description: string;
1188
1225
  }>;
1226
+ listSkillsForTenant(tenantId: string | undefined | null): Promise<Array<{
1227
+ name: string;
1228
+ description: string;
1229
+ }>>;
1189
1230
  /**
1190
1231
  * Wraps the run() generator with an OTel root span (invoke_agent) so all
1191
1232
  * child spans (LLM calls via AI SDK, tool execution) group under one trace.
@@ -1879,4 +1920,46 @@ declare class AgentOrchestrator {
1879
1920
  recoverStaleSubagents(): Promise<void>;
1880
1921
  }
1881
1922
 
1882
- 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 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, slugifyStorageComponent, startOpenAICodexDeviceAuth, verifyTenantToken, withToolResultArchiveParam, writeOpenAICodexSession };
1923
+ interface RunConversationTurnOpts {
1924
+ /** Initialised harness instance. */
1925
+ harness: AgentHarness;
1926
+ /** Conversation store backing the turn (typically `engine.conversations` from a StorageEngine). */
1927
+ conversationStore: ConversationStore;
1928
+ conversationId: string;
1929
+ /** The user's new message text. Required (use `""` if you only want to attach files). */
1930
+ task: string;
1931
+ /**
1932
+ * Optional file attachments (FileInput.data is base64 / data URI / https URL).
1933
+ * Files are uploaded via `harness.uploadStore` first so the persisted user
1934
+ * message references stable URLs instead of fat base64 blobs.
1935
+ */
1936
+ files?: FileInput[];
1937
+ /**
1938
+ * Extra parameters merged into runInput.parameters. Use this for recall
1939
+ * corpus, archive lookup keys, messaging metadata, etc. Do NOT include
1940
+ * `__activeConversationId`, `__ownerId`, or the tool-result-archive — the
1941
+ * helper sets those itself.
1942
+ */
1943
+ parameters?: Record<string, unknown>;
1944
+ abortSignal?: AbortSignal;
1945
+ tenantId?: string | null;
1946
+ /** Per-event hook — called for every AgentEvent yielded by the run, in order. */
1947
+ onEvent?: (event: AgentEvent) => void | Promise<void>;
1948
+ }
1949
+ interface RunConversationTurnResult {
1950
+ /** runId of the most recent run started during this turn. */
1951
+ latestRunId: string;
1952
+ /** True if the run was cancelled (via abortSignal or run:cancelled event). */
1953
+ cancelled: boolean;
1954
+ /** True if the run errored. The error has been emitted via onEvent as run:error. */
1955
+ errored: boolean;
1956
+ /** True if the run requested a continuation. Caller is responsible for triggering the continuation. */
1957
+ continuation: boolean;
1958
+ /** True if the run paused at a tool-approval checkpoint. */
1959
+ checkpointed: boolean;
1960
+ contextTokens: number;
1961
+ contextWindow: number;
1962
+ }
1963
+ declare const runConversationTurn: (opts: RunConversationTurnOpts) => Promise<RunConversationTurnResult>;
1964
+
1965
+ 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, DEFAULT_AGENT_DESCRIPTION, DEFAULT_AGENT_NAME, DEFAULT_MAX_STEPS, DEFAULT_MODEL_NAME, DEFAULT_MODEL_PROVIDER, 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 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, defaultAgentDefinition, 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
@@ -565,6 +565,53 @@ var loadPonchoConfig = async (workingDir) => {
565
565
  }
566
566
  };
567
567
 
568
+ // src/default-agent.ts
569
+ import { randomBytes } from "crypto";
570
+ var DEFAULT_AGENT_NAME = "agent";
571
+ var DEFAULT_AGENT_DESCRIPTION = "A helpful Poncho assistant";
572
+ var DEFAULT_MODEL_PROVIDER = "anthropic";
573
+ var DEFAULT_MODEL_NAME = "claude-opus-4-5";
574
+ var DEFAULT_TEMPERATURE = 0.2;
575
+ var DEFAULT_MAX_STEPS = 20;
576
+ var DEFAULT_TIMEOUT = 300;
577
+ var defaultAgentDefinition = (opts = {}) => {
578
+ const name = opts.name ?? DEFAULT_AGENT_NAME;
579
+ const id = opts.id ?? `agent_${randomBytes(16).toString("hex")}`;
580
+ const description = opts.description ?? DEFAULT_AGENT_DESCRIPTION;
581
+ const modelProvider = opts.modelProvider ?? DEFAULT_MODEL_PROVIDER;
582
+ const modelName = opts.modelName ?? DEFAULT_MODEL_NAME;
583
+ const temperature = opts.temperature ?? DEFAULT_TEMPERATURE;
584
+ const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
585
+ const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
586
+ return `---
587
+ name: ${name}
588
+ id: ${id}
589
+ description: ${description}
590
+ model:
591
+ provider: ${modelProvider}
592
+ name: ${modelName}
593
+ temperature: ${temperature}
594
+ limits:
595
+ maxSteps: ${maxSteps}
596
+ timeout: ${timeout}
597
+ ---
598
+
599
+ # {{name}}
600
+
601
+ You are **{{name}}**, a helpful assistant built with Poncho.
602
+
603
+ Working directory: {{runtime.workingDir}}
604
+ Environment: {{runtime.environment}}
605
+
606
+ ## Task Guidance
607
+
608
+ - Use tools when needed
609
+ - Explain your reasoning clearly
610
+ - Ask clarifying questions when requirements are ambiguous
611
+ - Never claim a file/tool change unless the corresponding tool call actually succeeded
612
+ `;
613
+ };
614
+
568
615
  // src/default-tools.ts
569
616
  import { mkdir, readdir, readFile as readFile3, rm, unlink, writeFile as writeFile2 } from "fs/promises";
570
617
  import { dirname, resolve as resolve4, sep } from "path";
@@ -5562,14 +5609,14 @@ var createTodoTools = (store) => {
5562
5609
  };
5563
5610
 
5564
5611
  // src/secrets-store.ts
5565
- import { createCipheriv, createDecipheriv, randomBytes, createHash as createHash3 } from "crypto";
5612
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes2, createHash as createHash3 } from "crypto";
5566
5613
  import { mkdir as mkdir3, readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
5567
5614
  import { dirname as dirname3, resolve as resolve7 } from "path";
5568
5615
  function deriveKey(signingKey) {
5569
5616
  return createHash3("sha256").update("poncho-secrets-v1:" + signingKey).digest();
5570
5617
  }
5571
5618
  function encrypt(plaintext, key) {
5572
- const iv = randomBytes(12);
5619
+ const iv = randomBytes2(12);
5573
5620
  const cipher = createCipheriv("aes-256-gcm", key, iv);
5574
5621
  const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
5575
5622
  const tag = cipher.getAuthTag();
@@ -9654,6 +9701,10 @@ var AgentHarness = class _AgentHarness {
9654
9701
  listSkills() {
9655
9702
  return this.loadedSkills.map((s) => ({ name: s.name, description: s.description }));
9656
9703
  }
9704
+ async listSkillsForTenant(tenantId) {
9705
+ const skills = await this.getSkillsForTenant(tenantId);
9706
+ return skills.map((s) => ({ name: s.name, description: s.description }));
9707
+ }
9657
9708
  /**
9658
9709
  * Wraps the run() generator with an OTel root span (invoke_agent) so all
9659
9710
  * child spans (LLM calls via AI SDK, tool execution) group under one trace.
@@ -12991,6 +13042,311 @@ ${resultBody}`,
12991
13042
  }
12992
13043
  };
12993
13044
 
13045
+ // src/orchestrator/run-conversation-turn.ts
13046
+ import { randomUUID as randomUUID6 } from "crypto";
13047
+ import { createLogger as createLogger7 } from "@poncho-ai/sdk";
13048
+ var log = createLogger7("orchestrator");
13049
+ var runConversationTurn = async (opts) => {
13050
+ const conversation = await opts.conversationStore.getWithArchive(opts.conversationId);
13051
+ if (!conversation) {
13052
+ throw new Error(`Conversation not found: ${opts.conversationId}`);
13053
+ }
13054
+ const canonicalHistory = resolveRunRequest(conversation, {
13055
+ conversationId: opts.conversationId,
13056
+ messages: conversation.messages
13057
+ });
13058
+ const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
13059
+ const harnessMessages = [...canonicalHistory.messages];
13060
+ const historyMessages = [...conversation.messages];
13061
+ const preRunMessages = [...conversation.messages];
13062
+ let userContent = opts.task;
13063
+ if (opts.files && opts.files.length > 0 && opts.harness.uploadStore) {
13064
+ const uploadedParts = await Promise.all(
13065
+ opts.files.map(async (f) => {
13066
+ const buf = Buffer.from(f.data, "base64");
13067
+ const key = deriveUploadKey(buf, f.mediaType);
13068
+ const ref = await opts.harness.uploadStore.put(key, buf, f.mediaType);
13069
+ return {
13070
+ type: "file",
13071
+ data: ref,
13072
+ mediaType: f.mediaType,
13073
+ filename: f.filename
13074
+ };
13075
+ })
13076
+ );
13077
+ userContent = [
13078
+ { type: "text", text: opts.task },
13079
+ ...uploadedParts
13080
+ ];
13081
+ }
13082
+ const turnTimestamp = Date.now();
13083
+ const userMessage = {
13084
+ role: "user",
13085
+ content: userContent,
13086
+ metadata: { id: randomUUID6(), timestamp: turnTimestamp }
13087
+ };
13088
+ const assistantId = randomUUID6();
13089
+ const draft = createTurnDraftState();
13090
+ let latestRunId = conversation.runtimeRunId ?? "";
13091
+ let runCancelled = false;
13092
+ let runContinuationMessages;
13093
+ let cancelHarnessMessages;
13094
+ let checkpointedRun = false;
13095
+ const buildMessages = () => {
13096
+ const draftSections = cloneSections(draft.sections);
13097
+ if (draft.currentTools.length > 0) {
13098
+ draftSections.push({ type: "tools", content: [...draft.currentTools] });
13099
+ }
13100
+ if (draft.currentText.length > 0) {
13101
+ draftSections.push({ type: "text", content: draft.currentText });
13102
+ }
13103
+ const userTurn = [userMessage];
13104
+ const hasDraftContent = draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
13105
+ if (!hasDraftContent) {
13106
+ return [...historyMessages, ...userTurn];
13107
+ }
13108
+ return [
13109
+ ...historyMessages,
13110
+ ...userTurn,
13111
+ {
13112
+ role: "assistant",
13113
+ content: draft.assistantResponse,
13114
+ metadata: buildAssistantMetadata(draft, draftSections, {
13115
+ id: assistantId,
13116
+ timestamp: turnTimestamp
13117
+ })
13118
+ }
13119
+ ];
13120
+ };
13121
+ const persistDraft = async () => {
13122
+ if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
13123
+ conversation.messages = buildMessages();
13124
+ conversation.updatedAt = Date.now();
13125
+ await opts.conversationStore.update(conversation);
13126
+ };
13127
+ conversation.messages = [...historyMessages, userMessage];
13128
+ conversation.subagentCallbackCount = 0;
13129
+ conversation._continuationCount = void 0;
13130
+ conversation.updatedAt = Date.now();
13131
+ opts.conversationStore.update(conversation).catch((err) => {
13132
+ log.error(
13133
+ `failed to persist user turn: ${err instanceof Error ? err.message : String(err)}`
13134
+ );
13135
+ });
13136
+ try {
13137
+ const execution = await executeConversationTurn({
13138
+ harness: opts.harness,
13139
+ runInput: {
13140
+ task: opts.task,
13141
+ conversationId: opts.conversationId,
13142
+ tenantId: opts.tenantId ?? void 0,
13143
+ parameters: withToolResultArchiveParam(
13144
+ {
13145
+ ...opts.parameters ?? {},
13146
+ __activeConversationId: opts.conversationId,
13147
+ __ownerId: conversation.ownerId
13148
+ },
13149
+ conversation
13150
+ ),
13151
+ messages: harnessMessages,
13152
+ files: opts.files && opts.files.length > 0 ? opts.files : void 0,
13153
+ abortSignal: opts.abortSignal
13154
+ },
13155
+ initialContextTokens: conversation.contextTokens ?? 0,
13156
+ initialContextWindow: conversation.contextWindow ?? 0,
13157
+ onEvent: async (event, eventDraft) => {
13158
+ draft.assistantResponse = eventDraft.assistantResponse;
13159
+ draft.toolTimeline = eventDraft.toolTimeline;
13160
+ draft.sections = eventDraft.sections;
13161
+ draft.currentTools = eventDraft.currentTools;
13162
+ draft.currentText = eventDraft.currentText;
13163
+ if (event.type === "run:started") {
13164
+ latestRunId = event.runId;
13165
+ }
13166
+ if (event.type === "run:cancelled") {
13167
+ runCancelled = true;
13168
+ if (event.messages) cancelHarnessMessages = event.messages;
13169
+ }
13170
+ if (event.type === "compaction:completed") {
13171
+ if (event.compactedMessages) {
13172
+ historyMessages.length = 0;
13173
+ historyMessages.push(...event.compactedMessages);
13174
+ const preservedFromHistory = historyMessages.length - 1;
13175
+ const removedCount = preRunMessages.length - Math.max(0, preservedFromHistory);
13176
+ const existingHistory = conversation.compactedHistory ?? [];
13177
+ conversation.compactedHistory = [
13178
+ ...existingHistory,
13179
+ ...preRunMessages.slice(0, removedCount)
13180
+ ];
13181
+ }
13182
+ }
13183
+ if (event.type === "step:completed") {
13184
+ await persistDraft();
13185
+ }
13186
+ if (event.type === "tool:approval:required") {
13187
+ const toolText = `- approval required \`${event.tool}\``;
13188
+ draft.toolTimeline.push(toolText);
13189
+ draft.currentTools.push(toolText);
13190
+ const existing = Array.isArray(conversation.pendingApprovals) ? conversation.pendingApprovals : [];
13191
+ if (!existing.some((a) => a.approvalId === event.approvalId)) {
13192
+ conversation.pendingApprovals = [
13193
+ ...existing,
13194
+ {
13195
+ approvalId: event.approvalId,
13196
+ runId: latestRunId || conversation.runtimeRunId || "",
13197
+ tool: event.tool,
13198
+ toolCallId: void 0,
13199
+ input: event.input ?? {},
13200
+ checkpointMessages: void 0,
13201
+ baseMessageCount: historyMessages.length,
13202
+ pendingToolCalls: []
13203
+ }
13204
+ ];
13205
+ conversation.updatedAt = Date.now();
13206
+ await opts.conversationStore.update(conversation);
13207
+ }
13208
+ await persistDraft();
13209
+ }
13210
+ if (event.type === "tool:approval:checkpoint") {
13211
+ conversation.messages = buildMessages();
13212
+ conversation.pendingApprovals = buildApprovalCheckpoints({
13213
+ approvals: event.approvals,
13214
+ runId: latestRunId,
13215
+ checkpointMessages: event.checkpointMessages,
13216
+ baseMessageCount: historyMessages.length,
13217
+ pendingToolCalls: event.pendingToolCalls
13218
+ });
13219
+ conversation._toolResultArchive = opts.harness.getToolResultArchive(
13220
+ opts.conversationId
13221
+ );
13222
+ conversation.updatedAt = Date.now();
13223
+ await opts.conversationStore.update(conversation);
13224
+ checkpointedRun = true;
13225
+ }
13226
+ if (event.type === "run:completed") {
13227
+ if (event.result.continuation && event.result.continuationMessages) {
13228
+ runContinuationMessages = event.result.continuationMessages;
13229
+ conversation.messages = buildMessages();
13230
+ conversation._continuationMessages = runContinuationMessages;
13231
+ conversation._harnessMessages = runContinuationMessages;
13232
+ conversation._toolResultArchive = opts.harness.getToolResultArchive(
13233
+ opts.conversationId
13234
+ );
13235
+ conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
13236
+ if (!checkpointedRun) {
13237
+ conversation.pendingApprovals = [];
13238
+ }
13239
+ if ((event.result.contextTokens ?? 0) > 0) {
13240
+ conversation.contextTokens = event.result.contextTokens;
13241
+ }
13242
+ if ((event.result.contextWindow ?? 0) > 0) {
13243
+ conversation.contextWindow = event.result.contextWindow;
13244
+ }
13245
+ conversation.updatedAt = Date.now();
13246
+ await opts.conversationStore.update(conversation);
13247
+ }
13248
+ }
13249
+ if (opts.onEvent) {
13250
+ await opts.onEvent(event);
13251
+ }
13252
+ }
13253
+ });
13254
+ flushTurnDraft(draft);
13255
+ latestRunId = execution.latestRunId || latestRunId;
13256
+ if (!checkpointedRun && !runContinuationMessages) {
13257
+ conversation.messages = buildMessages();
13258
+ applyTurnMetadata(
13259
+ conversation,
13260
+ {
13261
+ latestRunId,
13262
+ contextTokens: execution.runContextTokens,
13263
+ contextWindow: execution.runContextWindow,
13264
+ harnessMessages: execution.runHarnessMessages,
13265
+ toolResultArchive: opts.harness.getToolResultArchive(opts.conversationId)
13266
+ },
13267
+ { shouldRebuildCanonical }
13268
+ );
13269
+ await opts.conversationStore.update(conversation);
13270
+ }
13271
+ return {
13272
+ latestRunId,
13273
+ cancelled: runCancelled,
13274
+ errored: false,
13275
+ continuation: !!runContinuationMessages,
13276
+ checkpointed: checkpointedRun,
13277
+ contextTokens: execution.runContextTokens,
13278
+ contextWindow: execution.runContextWindow
13279
+ };
13280
+ } catch (error) {
13281
+ flushTurnDraft(draft);
13282
+ const aborted = opts.abortSignal?.aborted === true;
13283
+ if (aborted || runCancelled) {
13284
+ if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
13285
+ conversation.messages = buildMessages();
13286
+ applyTurnMetadata(
13287
+ conversation,
13288
+ {
13289
+ latestRunId,
13290
+ contextTokens: 0,
13291
+ contextWindow: 0,
13292
+ harnessMessages: cancelHarnessMessages,
13293
+ toolResultArchive: opts.harness.getToolResultArchive(opts.conversationId)
13294
+ },
13295
+ { shouldRebuildCanonical: true }
13296
+ );
13297
+ await opts.conversationStore.update(conversation);
13298
+ }
13299
+ if (!checkpointedRun) {
13300
+ const fresh = await opts.conversationStore.get(opts.conversationId);
13301
+ if (fresh && Array.isArray(fresh.pendingApprovals) && fresh.pendingApprovals.length > 0) {
13302
+ fresh.pendingApprovals = [];
13303
+ await opts.conversationStore.update(fresh);
13304
+ }
13305
+ }
13306
+ return {
13307
+ latestRunId,
13308
+ cancelled: true,
13309
+ errored: false,
13310
+ continuation: false,
13311
+ checkpointed: checkpointedRun,
13312
+ contextTokens: 0,
13313
+ contextWindow: 0
13314
+ };
13315
+ }
13316
+ const errorEvent = {
13317
+ type: "run:error",
13318
+ runId: latestRunId || "run_unknown",
13319
+ error: {
13320
+ code: "RUN_ERROR",
13321
+ message: error instanceof Error ? error.message : "Unknown error"
13322
+ }
13323
+ };
13324
+ if (opts.onEvent) {
13325
+ try {
13326
+ await opts.onEvent(errorEvent);
13327
+ } catch (hookErr) {
13328
+ log.error(
13329
+ `onEvent threw on run:error: ${hookErr instanceof Error ? hookErr.message : String(hookErr)}`
13330
+ );
13331
+ }
13332
+ }
13333
+ if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
13334
+ conversation.messages = buildMessages();
13335
+ conversation.updatedAt = Date.now();
13336
+ await opts.conversationStore.update(conversation);
13337
+ }
13338
+ return {
13339
+ latestRunId,
13340
+ cancelled: false,
13341
+ errored: true,
13342
+ continuation: false,
13343
+ checkpointed: checkpointedRun,
13344
+ contextTokens: 0,
13345
+ contextWindow: 0
13346
+ };
13347
+ }
13348
+ };
13349
+
12994
13350
  // src/index.ts
12995
13351
  import { defineTool as defineTool13 } from "@poncho-ai/sdk";
12996
13352
  export {
@@ -12998,6 +13354,13 @@ export {
12998
13354
  AgentOrchestrator,
12999
13355
  BashEnvironmentManager,
13000
13356
  CALLBACK_LOCK_STALE_MS,
13357
+ DEFAULT_AGENT_DESCRIPTION,
13358
+ DEFAULT_AGENT_NAME,
13359
+ DEFAULT_MAX_STEPS,
13360
+ DEFAULT_MODEL_NAME,
13361
+ DEFAULT_MODEL_PROVIDER,
13362
+ DEFAULT_TEMPERATURE,
13363
+ DEFAULT_TIMEOUT,
13001
13364
  InMemoryConversationStore,
13002
13365
  InMemoryEngine,
13003
13366
  InMemoryStateStore,
@@ -13054,6 +13417,7 @@ export {
13054
13417
  createTurnDraftState,
13055
13418
  createUploadStore,
13056
13419
  createWriteTool,
13420
+ defaultAgentDefinition,
13057
13421
  defineTool13 as defineTool,
13058
13422
  deleteOpenAICodexSession,
13059
13423
  deriveUploadKey,
@@ -13098,6 +13462,7 @@ export {
13098
13462
  resolveRunRequest,
13099
13463
  resolveSkillDirs,
13100
13464
  resolveStateConfig,
13465
+ runConversationTurn,
13101
13466
  slugifyStorageComponent,
13102
13467
  startOpenAICodexDeviceAuth,
13103
13468
  verifyTenantToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.41.0",
3
+ "version": "0.43.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,89 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Canonical default agent definition.
3
+ //
4
+ // This is the same agent.md a fresh `poncho init` produces. The CLI's
5
+ // AGENT_TEMPLATE in `packages/cli/src/templates.ts` delegates to this helper
6
+ // so there is exactly one source of truth, and SDK consumers (PonchOS, custom
7
+ // servers, etc.) can pass the same default to `new AgentHarness({ agentDefinition: ... })`
8
+ // without hand-copying the template.
9
+ // ---------------------------------------------------------------------------
10
+
11
+ import { randomBytes } from "node:crypto";
12
+
13
+ export interface DefaultAgentDefinitionOptions {
14
+ /** Display name for the agent. Default: "agent". */
15
+ name?: string;
16
+ /**
17
+ * Stable identifier embedded in the frontmatter. Default: a fresh
18
+ * `agent_<32hex>`. Note: when an injected `StorageEngine` is also passed
19
+ * to `AgentHarness`, the engine's `agentId` overrides this at runtime, so
20
+ * SDK consumers can leave it default.
21
+ */
22
+ id?: string;
23
+ /** Frontmatter description. Default: "A helpful Poncho assistant". */
24
+ description?: string;
25
+ /** Model provider. Default: "anthropic". */
26
+ modelProvider?: "anthropic" | "openai" | "openai-codex";
27
+ /** Model name. Default: "claude-opus-4-5". */
28
+ modelName?: string;
29
+ /** Sampling temperature. Default: 0.2. */
30
+ temperature?: number;
31
+ /** Max tool-call steps per run. Default: 20. */
32
+ maxSteps?: number;
33
+ /** Hard timeout in seconds. Default: 300. */
34
+ timeout?: number;
35
+ }
36
+
37
+ export const DEFAULT_AGENT_NAME = "agent";
38
+ export const DEFAULT_AGENT_DESCRIPTION = "A helpful Poncho assistant";
39
+ export const DEFAULT_MODEL_PROVIDER = "anthropic" as const;
40
+ export const DEFAULT_MODEL_NAME = "claude-opus-4-5";
41
+ export const DEFAULT_TEMPERATURE = 0.2;
42
+ export const DEFAULT_MAX_STEPS = 20;
43
+ export const DEFAULT_TIMEOUT = 300;
44
+
45
+ /**
46
+ * Returns the canonical default agent definition as a markdown string,
47
+ * ready to pass to `new AgentHarness({ agentDefinition })`. This is the
48
+ * exact same template `poncho init` writes to `AGENT.md`.
49
+ */
50
+ export const defaultAgentDefinition = (
51
+ opts: DefaultAgentDefinitionOptions = {},
52
+ ): string => {
53
+ const name = opts.name ?? DEFAULT_AGENT_NAME;
54
+ const id = opts.id ?? `agent_${randomBytes(16).toString("hex")}`;
55
+ const description = opts.description ?? DEFAULT_AGENT_DESCRIPTION;
56
+ const modelProvider = opts.modelProvider ?? DEFAULT_MODEL_PROVIDER;
57
+ const modelName = opts.modelName ?? DEFAULT_MODEL_NAME;
58
+ const temperature = opts.temperature ?? DEFAULT_TEMPERATURE;
59
+ const maxSteps = opts.maxSteps ?? DEFAULT_MAX_STEPS;
60
+ const timeout = opts.timeout ?? DEFAULT_TIMEOUT;
61
+
62
+ return `---
63
+ name: ${name}
64
+ id: ${id}
65
+ description: ${description}
66
+ model:
67
+ provider: ${modelProvider}
68
+ name: ${modelName}
69
+ temperature: ${temperature}
70
+ limits:
71
+ maxSteps: ${maxSteps}
72
+ timeout: ${timeout}
73
+ ---
74
+
75
+ # {{name}}
76
+
77
+ You are **{{name}}**, a helpful assistant built with Poncho.
78
+
79
+ Working directory: {{runtime.workingDir}}
80
+ Environment: {{runtime.environment}}
81
+
82
+ ## Task Guidance
83
+
84
+ - Use tools when needed
85
+ - Explain your reasoning clearly
86
+ - Ask clarifying questions when requirements are ambiguous
87
+ - Never claim a file/tool change unless the corresponding tool call actually succeeded
88
+ `;
89
+ };
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.
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from "./agent-parser.js";
2
2
  export * from "./agent-identity.js";
3
3
  export * from "./compaction.js";
4
4
  export * from "./config.js";
5
+ export * from "./default-agent.js";
5
6
  export * from "./default-tools.js";
6
7
  export * from "./harness.js";
7
8
  export * from "./memory.js";
@@ -52,3 +52,9 @@ export {
52
52
  type ContinuationHooks,
53
53
  type OrchestratorOptions,
54
54
  } from "./orchestrator.js";
55
+
56
+ export {
57
+ runConversationTurn,
58
+ type RunConversationTurnOpts,
59
+ type RunConversationTurnResult,
60
+ } from "./run-conversation-turn.js";
@@ -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
+ };