From 68217e8abe1b842950a9466a6ecc4fb220580f49 Mon Sep 17 00:00:00 2001 From: Francisco Pizarro Date: Thu, 13 Aug 2026 14:41:19 -0400 Subject: [PATCH 1/2] feat(pi): update Pi runtime, streaming, and chat flow fixes - Bump @earendil-works/pi-coding-agent 0.83.0 -> 0.84.1 via workspace catalog - Forward model samplingParams through worker provider registration - Stream bash tool output live into the tool block (bashUpdate) - Terminate the tool batch when a permission is denied - Emit sessions.status.changed (generating/idle) on Pi and ACP turn start/end - Ack chat.sendMessage immediately; stream without per-delta DB writes - Track real thinking time (reasoning_time) on Pi and ACP thinking blocks - Show a pending assistant message during worker startup in the chat UI - Add SDD docs for pi-model-sampling-params, pi-worker-bash-streaming, pi-worker-permission-terminate - Add .gitignore entry for local config files Co-authored-by: CommandCodeBot --- .agents/skills/pi-update/SKILL.md | 134 ++++++++++++++++++ .gitignore | 2 + apps/daemon/package.json | 2 +- .../daemon/src/host/acp-provider-execution.ts | 48 ++++++- .../daemon/src/host/bun-session-repository.ts | 10 ++ apps/daemon/src/host/pi-provider-execution.ts | 120 +++++++++++++--- apps/daemon/src/host/piWorker.ts | 23 ++- apps/daemon/src/host/piWorkerProtocol.ts | 6 +- .../daemon/test/daemonConfigPresenter.test.ts | 21 +++ apps/daemon/test/piWorker.test.ts | 3 +- bun.lock | 23 ++- .../features/pi-model-sampling-params/plan.md | 44 ++++++ .../features/pi-model-sampling-params/spec.md | 36 +++++ .../pi-model-sampling-params/tasks.md | 10 ++ .../features/pi-worker-bash-streaming/plan.md | 34 +++++ .../features/pi-worker-bash-streaming/spec.md | 32 +++++ .../pi-worker-bash-streaming/tasks.md | 8 ++ .../pi-worker-permission-terminate/plan.md | 26 ++++ .../pi-worker-permission-terminate/spec.md | 32 +++++ .../pi-worker-permission-terminate/tasks.md | 5 + package.json | 1 + .../src/protocol/acpContentMapper.ts | 14 +- .../pi-orchestrator-extension/package.json | 2 +- .../types/presenters/legacy.presenters.d.ts | 2 + .../presenters/llmprovider.presenter.d.ts | 2 + .../components/settings/ModelConfigDialog.tsx | 44 +++++- packages/ui/src/pages/ChatPage.tsx | 7 +- 27 files changed, 646 insertions(+), 45 deletions(-) create mode 100644 .agents/skills/pi-update/SKILL.md create mode 100644 docs/features/pi-model-sampling-params/plan.md create mode 100644 docs/features/pi-model-sampling-params/spec.md create mode 100644 docs/features/pi-model-sampling-params/tasks.md create mode 100644 docs/features/pi-worker-bash-streaming/plan.md create mode 100644 docs/features/pi-worker-bash-streaming/spec.md create mode 100644 docs/features/pi-worker-bash-streaming/tasks.md create mode 100644 docs/features/pi-worker-permission-terminate/plan.md create mode 100644 docs/features/pi-worker-permission-terminate/spec.md create mode 100644 docs/features/pi-worker-permission-terminate/tasks.md diff --git a/.agents/skills/pi-update/SKILL.md b/.agents/skills/pi-update/SKILL.md new file mode 100644 index 000000000..1f2542817 --- /dev/null +++ b/.agents/skills/pi-update/SKILL.md @@ -0,0 +1,134 @@ +--- +name: pi-update +description: Update the pinned @earendil-works/pi-coding-agent package to the latest version in this repo and triage the upstream changelog. Use when the user says "update pi", "bump pi", "update @earendil-works/pi-coding-agent", "upgrade pi-coding-agent", "keep pi up to date", or mentions the Pi coding agent version, its changelog, or new Pi features/extensions to adopt. Always checks the upstream Pi coding-agent CHANGELOG (https://github.com/earendil-works/pi/blob/main/packages/coding-agent/CHANGELOG.md) for breaking changes and for new useful features Argos could implement. +--- + +# Update Pi Coding Agent + +Argos embeds `@earendil-works/pi-coding-agent` as its agent runtime. The version is managed in **one place**: the workspace catalog in the root `package.json` (`workspaces.catalog`). Packages reference it via `"catalog:"`, so bumping the catalog entry is the only edit needed. + +## Where Pi Is Referenced In This Repo + +- **Root `package.json`** — `workspaces.catalog` holds the single pinned version (keep it pinned, no `^`). This is the only place the version changes. +- `apps/daemon/package.json` and `packages/pi-orchestrator-extension/package.json` — both declare `"@earendil-works/pi-coding-agent": "catalog:"`; do not edit versions here. +- `apps/daemon/src/host/piWorker.ts` — the Pi worker: creates the session, registers providers/models, bridges extensions, MCP tools, UI context, and maps `AgentSessionEvent`s to the daemon protocol. +- `packages/pi-orchestrator-extension/src/index.ts` — the `argos-orchestrator` inline extension that exposes Argos orchestration tools to Pi. +- `bun.lock` — lockfile records the resolved version and its transitive deps (`@earendil-works/pi-agent-core`, `pi-ai`, `pi-tui`, `pi-client`, `pi-protocol`, `pi-telemetry`). + +## Argos' Pi API Surface (verification reference) + +Triage and API-diffing are only fast because Argos uses a small, stable slice of Pi. Consult this map before analyzing a changelog entry or re-checking `.d.ts` files. + +### Used by `piWorker.ts` + +- `createAgentSession({ cwd, agentDir, modelRuntime, model, thinkingLevel, excludeTools, customTools, resourceLoader, sessionManager, settingsManager })` → `{ session, extensionsResult?, modelFallbackMessage? }`. +- `DefaultResourceLoader({ cwd, agentDir, settingsManager, systemPromptOverride, extensionFactories })`; `reload({ resolveProjectTrust })`; `getExtensions()` (`.errors`), `getSkills()`/`getPrompts()` (`.diagnostics`). +- `ModelRuntime.create({ authPath, modelsPath, allowModelNetwork })`, `.registerProvider(id, { name, baseUrl, api, headers, models })`, `.setRuntimeApiKey(id, apiKey)`, `.getModel(id, modelId)`. +- `SessionManager.create(cwd, sessionDir)` / `.open(sessionFile, sessionDir, cwd)`; `SettingsManager.create(cwd, agentDir)`. +- `AgentSession` methods: `prompt`, `steer`, `followUp`, `compact`, `abort`, `dispose`, `bindExtensions({ uiContext, mode: "rpc" })`, `subscribe`, `getSessionStats()` (`tokens.{input,output,cacheRead,cacheWrite}` + `cost`), `sessionFile`. +- `AgentSessionEvent` members bridged to the protocol: `message_update` (reads only `assistantMessageEvent` deltas: `text_delta`/`thinking_delta`/`thinking_start`/`thinking_end`), `tool_execution_start` (`toolCallId`,`toolName`,`args`), `tool_execution_update`, `tool_execution_end`, `queue_update`, `compaction_start`/`compaction_end`, `auto_retry_start`/`auto_retry_end`, `message_end` (`message.role`, `message.timestamp`), `agent_settled`. +- `ExtensionUIContext`: `createUiContext()` builds a full stub and **casts** it (`as ExtensionUIContext`), so new/renamed interface members do not fail typecheck; TUI-only methods route to `unsupported()`. Re-check the method list each update — a new member that Argos *should* bridge would silently be missing. + +### Used by `pi-orchestrator-extension` + +- `InlineExtension` (`{ name, factory }`), `defineTool`, `pi.registerTool(...)`, `pi.on(...)`. +- Tool `execute` returns `{ content, details, isError }`. Note `isError` is **not** a field of `AgentToolResult` (verified at 0.83 and 0.84) but typechecks; real tool errors should be thrown. + +### Never touched by Argos (skip unless an entry leaks into the surfaces above) + +- pi-ai internals: `ModelsStreamTransforms`→`ModelsRequestTransforms` renames, `ModelRegistry.*` (`getApiKeyAndHeaders`, `refresh`), `ModelsStore`, provider refresh context (`context.stored`/`context.publish`), OAuth `refreshToken`, `samplingParams`, deferred provider handles, vendor telemetry. +- pi-agent-core harness/session: v4 `Session`/`SessionStorage`/`SessionRepo`, `AgentHarness`, `JsonlSessionRepo`, `FileSystem.renameFile`, `RemoteSession`/`PiClient`/CBOR protocol. +- CLI/TUI: fullscreen mode, keybindings, terminal/theme/Mermaid/LaTeX rendering, `pi auth check`, slash commands, prompt history. +- Providers Argos does not surface (e.g. Baseten, Qwen token plan, Radius), `AI_AGENT=pi` env, `AGENTS.override.md`. + +**Caveats.** The map is a filter, not a guarantee: (1) always verify entries that touch a name appearing in `piWorker.ts`/`pi-orchestrator-extension` or a bridged `AgentSessionEvent` payload; (2) the map drifts as Argos adopts features — items move from "never touched" to "used", so refresh it when the skill mentions a new surface. + +## Workflow + +### 1. Determine Current And Latest Versions + +- Current: read the version in the workspace catalog (`workspaces.catalog` in the root `package.json`). It is the single source of truth; `apps/daemon` and `packages/pi-orchestrator-extension` both use `"catalog:"`. +- Latest: `bun pm view @earendil-works/pi-coding-agent version` and check `dist-tags` (`bun pm view @earendil-works/pi-coding-agent dist-tags --json`). Use the `latest` tag (not `legacy-node20`) unless the user asks otherwise. + +### 2. Read The Changelog Before Touching Anything + +Fetch the upstream changelog: + +``` +https://github.com/earendil-works/pi/blob/main/packages/coding-agent/CHANGELOG.md +``` + +(recommended: `https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/CHANGELOG.md` for the raw text). Read every version section **between the current pinned version (exclusive) and the target version (inclusive)**. If a section is truncated by the fetcher, re-fetch the raw file or query that specific version's section. + +For each version, classify each entry as one of: + +- **BREAKING / migration**: renamed exports, changed signatures, removed APIs, new required fields, event shape changes, dropped Node/Bun support, changed defaults. These are the priority — they can break `piWorker.ts`, `pi-orchestrator-extension`, or the piWorker protocol mapping. +- **New features / APIs**: new extension hooks, new `AgentSessionEvent` types, new `ExtensionUIContext` methods, new model/provider capabilities, new session APIs. Flag any that Argos could implement (e.g. new session events, new UI methods to bridge, new tool options, provider features). +- **Fixes / chores**: usually no action; note only if they change observable behavior Argos depends on. + +**Filter before deep-diving.** For every BREAKING entry, ask "does this touch a surface in the Argos API map above, or a type that leaks into one (e.g. the payload of a bridged `AgentSessionEvent`)?" Entries confined to the never-touched list get one line in the triage ("no Argos impact") and are skipped from `.d.ts` analysis. Worked example: 0.84.0's `message_update` "delta-only" change *looks* breaking but is Argos-compatible — Argos only ever reads `assistantMessageEvent` deltas and never used the removed cumulative `message`/`partial` fields. + +### 3. Produce A Triage Summary (Do This Before Editing) + +Write a concise per-version summary for the user: version(s), date(s), the breaking entries and which repo file they affect (or "no Argos impact"), and the candidate "new useful features to implement" with a one-line proposal each. Let the user pick which new features to adopt — do not silently implement them in the same change. + +### 4. Bump And Install + +1. Update the version in the workspace catalog (root `package.json`, `workspaces.catalog`) to the exact target version (keep it pinned, no `^`). No other `package.json` needs editing — both consumers use `"catalog:"`. +2. Run `bun install` to update `bun.lock`. Verify the resolved version and its transitive Pi deps moved together (check `bun.lock` for the `pi-coding-agent` entry and its `pi-agent-core`/`pi-ai`/`pi-tui` siblings; 0.84+ also adds `pi-client`/`pi-protocol`/`pi-telemetry`). + +### 5. Adapt Code To Breaking Changes + +Diff the upstream API against how this repo uses it (map above). Installed `.d.ts` locations (after `bun install`): + +- Main package: `apps/daemon/node_modules/@earendil-works/pi-coding-agent/dist/.d.ts` — **not** the repo-root `node_modules`. + - `index.d.ts` — full export list (use `rg '"..." from'` per surface). + - `core/sdk.d.ts` — `CreateAgentSessionOptions` / `createAgentSession`. + - `core/agent-session.d.ts` — `AgentSessionEvent` union, `SessionStats`, `ExtensionBindings`. + - `core/model-runtime.d.ts` — `ModelRuntime`. + - `core/extensions/types.d.ts` — `ToolDefinition`, `defineTool`, `InlineExtension`, `ExtensionAPI`, `ExtensionUIContext`, `ToolCallEvent`. + - `core/session-manager.d.ts`, `core/settings-manager.d.ts`, `core/resource-loader.d.ts`. +- Transitives (`pi-agent-core`, `pi-ai`, `pi-tui`, `pi-client`, `pi-protocol`): resolve under `node_modules/.bun/@earendil-works+@.../node_modules/...` — find via `Get-ChildItem -Recurse -Directory -Filter "pi-agent-core"`. `pi-agent-core/dist/types.d.ts` holds the `AgentEvent` union members (incl. `message_update`, `tool_execution_*`), `AgentToolResult`, `AgentMessage`. + +Checklist per surface: `createAgentSession` options, `AgentSessionEvent` union members, `ExtensionUIContext` method set, `defineTool` signature + result shape, `ModelRuntime` methods, `SessionManager`/`SettingsManager` statics, `DefaultResourceLoader` options + methods. + +Follow the repo's Specification-Driven Development workflow (see `.agents/skills/argos-sdd`) for any code adaptation needed. + +### 6. Validate + +- Type check: `bun run typecheck` (root script covers **only** `@argos/desktop`), **plus** `bun run typecheck` inside `apps/daemon` (its `tsconfig.json` includes `src/**`; `@argos/pi-orchestrator-extension` is typechecked transitively through the daemon graph — it has no own typecheck script). +- Lint / format (root): `bun run lint`, `bun run format`. +- Tests: `bun run test` (root runs `@argos/desktop` + `@argos/daemon` filters) or `cd apps/daemon && bun run test`. Root `test:main` targets the **desktop** suite; `@argos/daemon` has no `test:main` script. +- Known flake: `apps/daemon/test/piWorker.test.ts` can exceed vitest's default 5s test timeout when the full daemon suite runs in parallel (the worker's own ready-promise allows 10s; the test carries a 20s timeout). To tell a flake from real breakage, run it in isolation (`bunx vitest run test/piWorker.test.ts`, ~1s) or the whole suite with `--testTimeout 30000`. A real breakage also surfaces as `Worker error` events or stderr diagnostics, not just a timeout. + +If the Pi worker no longer starts or emits, or typecheck fails on the new API, stop and report the specific breaking entry from the changelog that caused it rather than patching around it silently. + +## Seeking Improvements (Be Proactive) + +A bump is a free audit window. After a successful update, scan the changelog for anything Argos could adopt or fix, and **report every item as a concrete proposal** (do not silently implement features): + +1. New `AgentSessionEvent` members → propose mapping in `piWorker.ts` + `piWorkerProtocol.ts` (e.g. new deltas, retry/summarization phases). +2. New `ExtensionUIContext` methods → propose bridging through the existing `uiRequest`/`uiResponse` protocol; TUI-only ones stay stubbed via `unsupported()`. +3. New extension APIs/hooks → e.g. `tool_call` `terminate` (0.84.1: lets blocked/denied tool batches skip the follow-up model call — pairs naturally with Argos permission mode), `registerMarkdownTransformer`, `AgentOptions.shouldStopAfterTurn`, new `registerTool` options, `scopedModels`. +4. New model/provider capabilities → e.g. `samplingParams`, new built-in providers, new `api` values; propose exposing via the daemon provider config (`apps/daemon/src/host/pi-provider-execution.ts`) or model settings UI. +5. New session/stat surfaces → e.g. new `SessionStats` fields or session APIs; propose surfacing in the usage dashboard / chat UI. +6. Security updates bundled in Pi's transitive deps (e.g. `undici`, `brace-expansion`) → call out in the report. +7. Repo friction hit during the update (flaky tests, misleading commands, docs drift, catalog-state surprises) → propose as follow-up fixes; apply the trivial infra ones in the same change. + +For each, give a one-line proposal + the files it touches. Recommend the highest-value adoption; let the user decide. + +## Response Rules + +- Always surface the changelog triage — never bump without reading it. The whole point is to catch breakage early and spot features worth implementing. +- Report the exact `X.Y.Z -> X.Y.Z` delta and the versions in between. +- Keep the package pinned at the exact version; do not switch to a range. +- If the user only asked to "check/update" without implementing features, deliver the triage summary and the bump, then list follow-up feature ideas for a later change. +- End every update with the triage **and** a "follow-up feature ideas" list (one line each, with files touched). Recommend the top pick. +- Follow conventional commit style (`fix(deps)` / `chore(deps)` for a plain bump; `feat` when adopting a new Pi feature). + +## Examples + +- "Update pi to latest" → run the full workflow: bump, changelog triage, install, typecheck/lint/test. +- "Is there a new version of @earendil-works/pi-coding-agent?" → compare versions and summarize the changelog delta only; do not edit yet. +- "Pi added a new session event, can we surface it?" → check the changelog section for the new `AgentSessionEvent` type and map it in `piWorker.ts`. +- "Any new Pi features worth having?" → run the changelog delta and produce the "Seeking Improvements" list only; no bump. \ No newline at end of file diff --git a/.gitignore b/.gitignore index 58a7f1c7b..5a9f1f52d 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ test-results/ *.bun-build .turbo/cache/* .turbo\cache + +*.local.json \ No newline at end of file diff --git a/apps/daemon/package.json b/apps/daemon/package.json index 8d71ff987..c9676e7e7 100644 --- a/apps/daemon/package.json +++ b/apps/daemon/package.json @@ -25,7 +25,7 @@ "@argos/shared-contracts": "workspace:*", "@argos/skills-runtime": "workspace:*", "@duckdb/node-api": "1.5.5-r.3", - "@earendil-works/pi-coding-agent": "0.83.0", + "@earendil-works/pi-coding-agent": "catalog:", "ai": "catalog:", "chokidar": "^5.0.0", "fflate": "catalog:", diff --git a/apps/daemon/src/host/acp-provider-execution.ts b/apps/daemon/src/host/acp-provider-execution.ts index 941697cc4..af173f3b4 100644 --- a/apps/daemon/src/host/acp-provider-execution.ts +++ b/apps/daemon/src/host/acp-provider-execution.ts @@ -25,6 +25,7 @@ import type { BunSessionRepository } from "./bun-session-repository"; import { usageDateKey } from "./bun-session-repository"; import { createDaemonAcpPorts } from "./acpPorts"; import { createDaemonAcpSqlitePresenter } from "./daemonAcpSqlite"; +import { sessionsStatusChangedEvent } from "@argos/shared-contracts"; import { methods as acpMethods, PROTOCOL_VERSION } from "@agentclientprotocol/sdk"; import type { AcpConfigState, AcpAgentDiagnostics, AcpDebugRequest, AcpDebugRunResult } from "@argos/shared/presenter"; @@ -183,6 +184,14 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { doneResolve(); }); + await this.sessionRepository.setSessionStatus?.(sessionId, "generating"); + this.eventPublisher.publish(sessionsStatusChangedEvent.name, { + sessionId, + status: "generating", + reason: "generation-started", + version: 1, + }); + return { requestId, messageId: assistantMessageId }; } @@ -401,6 +410,7 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { ): Promise { const blocks: Array> = []; let planRevision = 0; + let reasoningStartTime: number | undefined; let lastUsage: { used: number; size: number; @@ -439,6 +449,7 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { } const now = Date.now(); + let reasoningAppeared = false; for (const block of mapped.blocks) { if (block.type === "plan") continue; const last = blocks.at(-1); @@ -446,12 +457,27 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { last.content = `${last.content ?? ""}${block.content ?? ""}`; } else if (block.type === "reasoning_content" && last?.type === "reasoning_content") { last.content = `${last.content ?? ""}${block.content ?? ""}`; + reasoningAppeared = true; + } else if (block.type === "reasoning_content") { + if (reasoningStartTime === undefined) { + reasoningStartTime = mapped.reasoningStartTime ?? now; + } + blocks.push(block); + reasoningAppeared = true; } else if (block.type === "content") { blocks.push({ type: "content", content: block.content ?? "", status: "loading", timestamp: now }); } else { blocks.push(block); } } + // Close the reasoning window when a chunk that is NOT reasoning arrives + // after reasoning started (text/tool/action means the thought stream ended). + if (reasoningStartTime !== undefined && !reasoningAppeared && mapped.blocks.some((b) => b.type !== "plan")) { + const lastReasoning = [...blocks].reverse().find((b) => b.type === "reasoning_content"); + if (lastReasoning && typeof lastReasoning.reasoning_time !== "object") { + lastReasoning.reasoning_time = { start: reasoningStartTime, end: now }; + } + } this.eventPublisher.publish("chat.stream.updated", { kind: "snapshot", @@ -465,12 +491,18 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { const replyBlocks = blocks.map((b) => (b.type === "content" ? { ...b, status: "success" } : b)); const usageMetadata = lastUsage ? { usage: lastUsage } : {}; + // Close an unclosed reasoning window (turn ended while still thinking). + if (reasoningStartTime !== undefined) { + const lastReasoning = [...replyBlocks].reverse().find((b) => b.type === "reasoning_content"); + if (lastReasoning && typeof lastReasoning.reasoning_time !== "object") { + lastReasoning.reasoning_time = { start: reasoningStartTime, end: Date.now() }; + } + } await this.sessionRepository.finalizeAssistantMessage( assistantMessageId, replyBlocks, JSON.stringify({ model: agent.id, provider: "acp", ...usageMetadata }), ); - if (lastUsage) { const costAmount = typeof lastUsage.cost?.amount === "number" && Number.isFinite(lastUsage.cost.amount) @@ -512,6 +544,13 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { messageId: assistantMessageId, completedAt: Date.now(), }); + await this.sessionRepository.setSessionStatus?.(sessionId, "idle"); + this.eventPublisher.publish(sessionsStatusChangedEvent.name, { + sessionId, + status: "idle", + reason: "generation-completed", + version: 1, + }); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); await this.sessionRepository.setMessageError( @@ -526,6 +565,13 @@ export class AcpProviderExecutionPort implements ProviderExecutionPort { failedAt: Date.now(), error: errorMsg, }); + await this.sessionRepository.setSessionStatus?.(sessionId, "idle"); + this.eventPublisher.publish(sessionsStatusChangedEvent.name, { + sessionId, + status: "idle", + reason: "generation-completed", + version: 1, + }); } } diff --git a/apps/daemon/src/host/bun-session-repository.ts b/apps/daemon/src/host/bun-session-repository.ts index 5d5823779..d2df7e635 100644 --- a/apps/daemon/src/host/bun-session-repository.ts +++ b/apps/daemon/src/host/bun-session-repository.ts @@ -1008,6 +1008,16 @@ export class BunSessionRepository implements SessionRepository { .run(Date.now()); } + async setSessionStatus( + sessionId: string, + status: "idle" | "generating" | "blocked" | "done" | "error", + ): Promise { + this.ensureSessionExists(sessionId); + this.db + .prepare("UPDATE daemon_sessions SET status = ?, updated_at = ? WHERE id = ?") + .run(status, Date.now(), sessionId); + } + async getActive(webContentsId: number): Promise { const row = this.db.prepare("SELECT * FROM daemon_sessions WHERE status = 'active' LIMIT 1").get() as any; return row ? this.toSessionWithState(row) : null; diff --git a/apps/daemon/src/host/pi-provider-execution.ts b/apps/daemon/src/host/pi-provider-execution.ts index c19332f24..dfdf94b89 100644 --- a/apps/daemon/src/host/pi-provider-execution.ts +++ b/apps/daemon/src/host/pi-provider-execution.ts @@ -4,6 +4,7 @@ import fs from "node:fs"; import path from "node:path"; import readline from "node:readline"; import type { IEventPublisher, ProviderExecutionPort } from "@argos/backend-core"; +import { sessionsStatusChangedEvent } from "@argos/shared-contracts"; import type { AssistantMessageBlock, MessageStartResult, @@ -35,6 +36,8 @@ interface ActiveTurn { blocks: AssistantMessageBlock[]; resolve: () => void; reject: (error: Error) => void; + /** Timestamp (ms) when thinking/reasoning started, if any. */ + thinkingStart?: number; } interface PiWorkerHandle { @@ -81,6 +84,7 @@ function workerProvider( ): PiWorkerProvider { const model = modelFor(provider, modelId); const cost = resolveModelCost(configPresenter, provider.id, modelId); + const samplingParams = configPresenter.getModelConfig(modelId, provider.id).samplingParams ?? model.samplingParams; return { id: provider.id, name: provider.name, @@ -94,6 +98,7 @@ function workerProvider( input: model.vision ? ["text", "image"] : ["text"], contextWindow: model.contextLength || 128_000, maxTokens: model.maxTokens || 8_192, + ...(samplingParams ? { samplingParams } : {}), ...(cost ? { cost } : {}), }, }; @@ -147,22 +152,54 @@ export class PiProviderExecutionPort implements ProviderExecutionPort { activeTurn = { commandId: requestId, requestId, messageId, blocks: [], resolve, reject }; worker.turn = activeTurn; }); + await this.markGenerating(sessionId); this.send(worker.process, { type: "prompt", id: requestId, text }); - try { - await completed; - return { requestId, messageId }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - await this.sessionRepository.setMessageError(messageId, activeTurn.blocks, JSON.stringify({ runtime: "pi" })); - this.eventPublisher.publish("chat.stream.failed", { - requestId, - sessionId, - messageId, - failedAt: Date.now(), - error: message, - }); - throw error; - } + + // Mirror ACP: the route acks immediately; the turn runs in the worker and + // streams via chat.stream.* events. Errors are published as chat.stream.failed. + void (async () => { + try { + await completed; + await this.markIdle(sessionId); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + try { + await this.sessionRepository.setMessageError(messageId, activeTurn.blocks, JSON.stringify({ runtime: "pi" })); + } catch { + // Best-effort; the failed event below is the source of truth for the UI. + } + this.eventPublisher.publish("chat.stream.failed", { + requestId, + sessionId, + messageId, + failedAt: Date.now(), + error: message, + }); + await this.markIdle(sessionId).catch(() => {}); + } + })(); + + return { requestId, messageId }; + } + + private async markGenerating(sessionId: string): Promise { + await this.sessionRepository.setSessionStatus(sessionId, "generating"); + this.eventPublisher.publish(sessionsStatusChangedEvent.name, { + sessionId, + status: "generating", + reason: "generation-started", + version: 1, + }); + } + + private async markIdle(sessionId: string): Promise { + await this.sessionRepository.setSessionStatus(sessionId, "idle"); + this.eventPublisher.publish(sessionsStatusChangedEvent.name, { + sessionId, + status: "idle", + reason: "generation-completed", + version: 1, + }); } getActiveGeneration(sessionId: string): { eventId: string; runId: string } | null { @@ -227,7 +264,7 @@ export class PiProviderExecutionPort implements ProviderExecutionPort { this.send(pending.worker, { type: "uiResponse", id: pending.workerRequestId, value }); if (block) block.status = value === undefined ? "denied" : "success"; } - if (turn) await this.publishSnapshot(sessionId, turn); + if (turn) this.publishSnapshot(sessionId, turn); return { resumed: true, handledInline: true }; } @@ -452,7 +489,18 @@ export class PiProviderExecutionPort implements ProviderExecutionPort { if (previous?.type === type && previous.status === "loading") previous.content = `${previous.content ?? ""}${event.text}`; else turn.blocks.push({ type, content: event.text, status: "loading", timestamp: Date.now() }); - await this.publishSnapshot(sessionId, turn); + this.publishSnapshot(sessionId, turn); + } else if (event.type === "thinkingStart") { + if (turn.thinkingStart === undefined) turn.thinkingStart = Date.now(); + } else if (event.type === "thinkingEnd") { + const start = turn.thinkingStart ?? Date.now(); + const end = Date.now(); + turn.thinkingStart = undefined; + const block = turn.blocks.at(-1); + if (block?.type === "reasoning_content" && block.status === "loading") { + block.reasoning_time = { start, end }; + } + this.publishSnapshot(sessionId, turn); } else if (event.type === "toolStart") { turn.blocks.push({ id: event.toolCallId, @@ -461,16 +509,40 @@ export class PiProviderExecutionPort implements ProviderExecutionPort { timestamp: Date.now(), tool_call: { id: event.toolCallId, name: event.toolName, params: JSON.stringify(event.input) }, }); - await this.publishSnapshot(sessionId, turn); + this.publishSnapshot(sessionId, turn); } else if (event.type === "toolEnd") { const block = turn.blocks.find((item) => item.id === event.toolCallId); if (block) { block.status = event.isError ? "error" : "success"; block.tool_call = { ...block.tool_call!, response: stringifyResult(event.result) }; } - await this.publishSnapshot(sessionId, turn); + this.publishSnapshot(sessionId, turn); + } else if (event.type === "bashUpdate") { + const target = + (event.toolCallId ? turn.blocks.find((item) => item.id === event.toolCallId) : undefined) ?? + [...turn.blocks].reverse().find((item) => item.type === "tool_call"); + if (target && target.status === "loading") { + target.tool_call = { + ...target.tool_call!, + response: `${target.tool_call?.response ?? ""}${event.delta}`, + }; + this.publishSnapshot(sessionId, turn); + } } else if (event.type === "settled") { for (const block of turn.blocks) if (block.status === "loading") block.status = "success"; + // If thinking never emitted thinkingEnd (e.g. the run ended mid-thought), + // close the window with the assistant message timestamp from the worker. + if (turn.thinkingStart !== undefined) { + const block = turn.blocks.find((item) => item.type === "reasoning_content" && item.status === "success"); + if (block) { + block.reasoning_time = { + start: turn.thinkingStart, + end: event.messageTimestamp ?? Date.now(), + }; + } + turn.thinkingStart = undefined; + } + worker.turn = undefined; await this.sessionRepository.finalizeAssistantMessage( turn.messageId, turn.blocks, @@ -483,7 +555,6 @@ export class PiProviderExecutionPort implements ProviderExecutionPort { completedAt: Date.now(), }); turn.resolve(); - worker.turn = undefined; if (event.sessionFile) this.sessionRepository.setPiSessionFile(sessionId, event.sessionFile); } } @@ -520,11 +591,14 @@ export class PiProviderExecutionPort implements ProviderExecutionPort { blockId, kind: event.type === "permissionRequest" ? "permission" : "ui", }); - void this.publishSnapshot(sessionId, turn); + this.publishSnapshot(sessionId, turn); } - private async publishSnapshot(sessionId: string, turn: ActiveTurn): Promise { - await this.sessionRepository.updateAssistantContent(turn.messageId, turn.blocks); + private publishSnapshot(sessionId: string, turn: ActiveTurn): void { + // Persistence is not on the streaming hot path; the final content is + // persisted by finalizeAssistantMessage on settled. A failed write here + // must not stall or drop the live stream. + void this.sessionRepository.updateAssistantContent(turn.messageId, turn.blocks).catch(() => {}); this.eventPublisher.publish("chat.stream.updated", { kind: "snapshot", requestId: turn.requestId, diff --git a/apps/daemon/src/host/piWorker.ts b/apps/daemon/src/host/piWorker.ts index 29c8b150e..6c335cf2a 100644 --- a/apps/daemon/src/host/piWorker.ts +++ b/apps/daemon/src/host/piWorker.ts @@ -26,6 +26,8 @@ if (process.argv.includes("--version")) { let session: AgentSession | undefined; let init: PiWorkerInit | undefined; let activeCommandId: string | undefined; +/** Timestamp (ms) of the last assistant message that completed streaming; used to compute thinking time. */ +let lastAssistantTimestamp: number | undefined; const pending = new Map void; reject: (error: Error) => void }>(); // `getSessionStats()` returns CUMULATIVE totals for the whole session. To store // per-turn usage we emit the delta since the last settled turn (keyed by @@ -85,7 +87,7 @@ function createHostExtension(config: PiWorkerInit): InlineExtension { toolName: event.toolName, input: event.input, }); - if (!granted) return { block: true, reason: "Denied by the user" }; + if (!granted) return { block: true, reason: "Denied by the user", terminate: true }; }); }, }; @@ -167,6 +169,8 @@ function handleSessionEvent(event: AgentSessionEvent): void { const part = event.assistantMessageEvent; if (part.type === "text_delta") emit({ type: "delta", kind: "text", text: part.delta }); if (part.type === "thinking_delta") emit({ type: "delta", kind: "thinking", text: part.delta }); + if (part.type === "thinking_start") emit({ type: "thinkingStart" }); + if (part.type === "thinking_end") emit({ type: "thinkingEnd" }); break; } case "tool_execution_start": @@ -189,6 +193,9 @@ function handleSessionEvent(event: AgentSessionEvent): void { isError: event.isError, }); break; + case "bash_execution_update": + emit({ type: "bashUpdate", toolCallId: event.id, delta: event.delta }); + break; case "queue_update": emit({ type: "queue", steering: event.steering, followUp: event.followUp }); break; @@ -204,6 +211,13 @@ function handleSessionEvent(event: AgentSessionEvent): void { case "auto_retry_end": emit({ type: "retry", phase: "end", attempt: event.attempt, error: event.finalError }); break; + case "message_end": { + const msg = event.message; + if (msg && msg.role === "assistant" && typeof msg.timestamp === "number") { + lastAssistantTimestamp = msg.timestamp; + } + break; + } case "agent_settled": { // Emit usage BEFORE settled: the daemon persists usage from the settled // handler, which runs synchronously when the settled event arrives. @@ -243,7 +257,12 @@ function handleSessionEvent(event: AgentSessionEvent): void { diagnostic(error, "usage"); } } - emit({ type: "settled", id: activeCommandId, sessionFile: session?.sessionFile }); + emit({ + type: "settled", + id: activeCommandId, + sessionFile: session?.sessionFile, + messageTimestamp: lastAssistantTimestamp, + }); activeCommandId = undefined; break; } diff --git a/apps/daemon/src/host/piWorkerProtocol.ts b/apps/daemon/src/host/piWorkerProtocol.ts index d63b7aadb..441d935d4 100644 --- a/apps/daemon/src/host/piWorkerProtocol.ts +++ b/apps/daemon/src/host/piWorkerProtocol.ts @@ -14,6 +14,7 @@ export interface PiWorkerProvider { input: Array<"text" | "image">; contextWindow: number; maxTokens: number; + samplingParams?: Record; cost?: { input: number; output: number; @@ -57,13 +58,16 @@ export type PiWorkerEvent = | { type: "ready"; sessionFile?: string; diagnostics: PiWorkerDiagnostic[] } | { type: "accepted"; id: string; sessionFile?: string } | { type: "delta"; kind: "text" | "thinking"; text: string } + | { type: "thinkingStart" } + | { type: "thinkingEnd" } | { type: "toolStart"; toolCallId: string; toolName: string; input: unknown } | { type: "toolUpdate"; toolCallId: string; toolName: string; partialResult: unknown } | { type: "toolEnd"; toolCallId: string; toolName: string; result: unknown; isError: boolean } + | { type: "bashUpdate"; toolCallId?: string; delta: string } | { type: "queue"; steering: readonly string[]; followUp: readonly string[] } | { type: "compaction"; phase: "start" | "end"; reason: string; error?: string } | { type: "retry"; phase: "start" | "end"; attempt: number; error?: string } - | { type: "settled"; id?: string; sessionFile?: string } + | { type: "settled"; id?: string; sessionFile?: string; messageTimestamp?: number } | { type: "error"; id?: string; message: string; stack?: string } | { type: "usage"; diff --git a/apps/daemon/test/daemonConfigPresenter.test.ts b/apps/daemon/test/daemonConfigPresenter.test.ts index 061343102..6ef907b40 100644 --- a/apps/daemon/test/daemonConfigPresenter.test.ts +++ b/apps/daemon/test/daemonConfigPresenter.test.ts @@ -133,4 +133,25 @@ describe("DaemonConfigPresenter", () => { const all = presenter.getModelStatusMap(); expect(all["deepseek:deepseek-reasoner"]).toBe(true); }); + + it("round-trips samplingParams through the model config store", async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "argos-daemon-config-")); + roots.push(root); + const { DaemonConfigPresenter } = await import("../src/host/daemonConfigPresenter"); + const presenter = new DaemonConfigPresenter(path.join(root, "config"), path.join(root, "data")); + + const saved = presenter.setModelConfig("my-model", "openai", { + samplingParams: { temperature: 0.3, top_p: 0.9, frequency_penalty: 0.2 }, + } as never); + + expect(saved.samplingParams).toEqual({ temperature: 0.3, top_p: 0.9, frequency_penalty: 0.2 }); + expect(presenter.getModelConfig("my-model", "openai").samplingParams).toEqual({ + temperature: 0.3, + top_p: 0.9, + frequency_penalty: 0.2, + }); + + // Absent configs default to undefined. + expect(presenter.getModelConfig("other-model", "openai").samplingParams).toBeUndefined(); + }); }); diff --git a/apps/daemon/test/piWorker.test.ts b/apps/daemon/test/piWorker.test.ts index cf501244f..5accbcde5 100644 --- a/apps/daemon/test/piWorker.test.ts +++ b/apps/daemon/test/piWorker.test.ts @@ -67,6 +67,7 @@ describe("Pi worker", () => { input: ["text"], contextWindow: 8_192, maxTokens: 1_024, + samplingParams: { temperature: 0.3, top_p: 0.9 }, }, }, disabledTools: [], @@ -86,5 +87,5 @@ describe("Pi worker", () => { child.stdin.write(`${JSON.stringify({ type: "dispose" })}\n`); child.kill(); } - }); + }, 20_000); }); diff --git a/bun.lock b/bun.lock index 3b600c4f9..12dea166e 100644 --- a/bun.lock +++ b/bun.lock @@ -46,7 +46,7 @@ "@argos/shared-contracts": "workspace:*", "@argos/skills-runtime": "workspace:*", "@duckdb/node-api": "1.5.5-r.3", - "@earendil-works/pi-coding-agent": "0.83.0", + "@earendil-works/pi-coding-agent": "catalog:", "ai": "catalog:", "chokidar": "^5.0.0", "fflate": "catalog:", @@ -266,7 +266,7 @@ "version": "0.1.0", "dependencies": { "@argos/shared": "workspace:*", - "@earendil-works/pi-coding-agent": "0.83.0", + "@earendil-works/pi-coding-agent": "catalog:", "typebox": "1.3.9", }, "devDependencies": { @@ -412,6 +412,7 @@ "@ai-sdk/openai": "^4.0.27", "@ai-sdk/openai-compatible": "^3.0.20", "@aws-sdk/client-bedrock": "^3.1101.0", + "@earendil-works/pi-coding-agent": "0.84.1", "@modelcontextprotocol/sdk": "^1.30.0", "@playwright/test": "^1.62.1", "@tailwindcss/vite": "^4.3.3", @@ -687,13 +688,19 @@ "@duckdb/node-bindings-win32-x64": ["@duckdb/node-bindings-win32-x64@1.5.5-r.3", "", { "os": "win32", "cpu": "x64" }, "sha512-NTODIfgfKARm86kOgSW5CvFElE3J+3X0Y0+3Y40P3biO4wrg4FBGtNSCaVyKuWe+CBkA5PODjgIo3w07hu5qrQ=="], - "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.83.0", "", { "dependencies": { "@earendil-works/pi-ai": "^0.83.0", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", "yaml": "2.9.0" } }, "sha512-RorGp9OH5l3ElpuC5a5ZQ2eWcchZGXflXRzVGkV99y3y6tT+LLNyxoYIdVKvTKWEObwhExeQbTH0fI2tE4iX4g=="], + "@earendil-works/pi-agent-core": ["@earendil-works/pi-agent-core@0.84.1", "", { "dependencies": { "@earendil-works/pi-ai": "^0.84.1", "@earendil-works/pi-telemetry": "^0.84.1", "diff": "8.0.4", "ignore": "7.0.5", "typebox": "1.3.7", "yaml": "2.9.0" } }, "sha512-evyzXYWCLQGmcaBYHlmSku02r8qoN4SGI60GZABo6iV+H+nqX+P9ud8fEZ4GmRq9mUSREvvfX+w9dA9ThF9C6w=="], - "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.83.0", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.3.7" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-m3IZD4g3er0V8TC9+Vpgw/sjTKqcJlkcIBy/JvsgRubuuik3tAVzyugUg4rVrShIkkOT69mEd34NEqKUIsl6JQ=="], + "@earendil-works/pi-ai": ["@earendil-works/pi-ai@0.84.1", "", { "dependencies": { "@anthropic-ai/sdk": "0.91.1", "@aws-sdk/client-bedrock-runtime": "3.1048.0", "@earendil-works/pi-telemetry": "^0.84.1", "@google/genai": "1.52.0", "@mistralai/mistralai": "2.2.6", "@opentelemetry/api": "1.9.0", "@smithy/node-http-handler": "4.7.3", "http-proxy-agent": "7.0.2", "https-proxy-agent": "7.0.6", "openai": "6.26.0", "partial-json": "0.1.7", "typebox": "1.3.7" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-wMsAdJMxuNri08vLqTyYVI201DQQezGhPSTkzYsHdw5dYX3rCNwEmSvpaAwhi7ELKI/2tE/CEgSWg/6iRxSgdQ=="], - "@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.83.0", "", { "dependencies": { "@earendil-works/pi-agent-core": "^0.83.0", "@earendil-works/pi-ai": "^0.83.0", "@earendil-works/pi-tui": "^0.83.0", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "glob": "13.0.6", "highlight.js": "10.7.3", "hosted-git-info": "9.0.3", "ignore": "7.0.5", "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.3.7", "undici": "8.5.0", "yaml": "2.9.0" }, "optionalDependencies": { "@mariozechner/clipboard": "0.3.9" }, "bin": { "pi": "dist/cli.js" } }, "sha512-uYhF+FsZxogoSX/AxBcUdiY+ZklubwaXyAoEGA2eQwsHcyEAhUYIKh/WLXe/a8+k8eTCmxb+ZN2Zo9mzQtzbWw=="], + "@earendil-works/pi-client": ["@earendil-works/pi-client@0.84.1", "", { "dependencies": { "@earendil-works/pi-protocol": "^0.84.1" } }, "sha512-/V5hGHE4Zq+jG0GtwIB9PyBUOGd6gBLZ7lkQYFKchKnxYHeH3rmWC5xw4kpnZKKBuBuFTdLVbU9vEjlAGMMb2A=="], - "@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.83.0", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" } }, "sha512-IoYrb0rORjELmEpNtoCA/U8je3KopMkRAVJRdSzvXRvgb+Huo1gNh8Q5CSZvNOiYtDxJdj2tYZZHZ4B3+IN3hA=="], + "@earendil-works/pi-coding-agent": ["@earendil-works/pi-coding-agent@0.84.1", "", { "dependencies": { "@earendil-works/pi-agent-core": "^0.84.1", "@earendil-works/pi-ai": "^0.84.1", "@earendil-works/pi-client": "^0.84.1", "@earendil-works/pi-protocol": "^0.84.1", "@earendil-works/pi-tui": "^0.84.1", "@silvia-odwyer/photon-node": "0.3.4", "chalk": "5.6.2", "cross-spawn": "7.0.6", "diff": "8.0.4", "glob": "13.0.6", "grok-mermaid": "0.2.2", "highlight.js": "10.7.3", "hosted-git-info": "9.0.3", "ignore": "7.0.5", "jiti": "2.7.0", "minimatch": "10.2.5", "proper-lockfile": "4.1.2", "semver": "7.8.0", "typebox": "1.3.7", "undici": "8.9.0", "yaml": "2.9.0" }, "optionalDependencies": { "@mariozechner/clipboard": "0.3.9" }, "bin": { "pi": "dist/cli.js" } }, "sha512-ncAqFrG+iybuPGOhMiZoEHkEzTpJgz3guYD32pD+M7ucc0WeHmauP6wa7qwP8V/KWvsZDVNa5XGsdZ7fkC7w7A=="], + + "@earendil-works/pi-protocol": ["@earendil-works/pi-protocol@0.84.1", "", { "dependencies": { "typebox": "1.3.7" } }, "sha512-Ox1pciyeSPGEEUcxvR0/dJcrY7C6hrEGA8y71rOsvSIUlXN1Cbp/be/eoL71OGDBk5O97TeQPfWN6Ju/2Ehjww=="], + + "@earendil-works/pi-telemetry": ["@earendil-works/pi-telemetry@0.84.1", "", {}, "sha512-180/xGJtsq7IoR3p9EKWjRd0e9M4DkxInhlo9xyD7prDC7Qrhqq+nhvwrW0lFjPfXcEI2FSHmGCSyvSJE9GsaQ=="], + + "@earendil-works/pi-tui": ["@earendil-works/pi-tui@0.84.1", "", { "dependencies": { "get-east-asian-width": "1.6.0", "marked": "18.0.5" } }, "sha512-udeXFbgEhJ6JiB0uguwNVNkDy2FENfmtQwPcY+/iJ8GWeq18wkal1tKqa5YyeH0IqtX1vG0cGh8zfSYzyzVuLA=="], "@electron-internal/extract-zip": ["@electron-internal/extract-zip@1.0.4", "", {}, "sha512-Zr1Vs7E9tpCNhZHDAbFVXc2gEVCG9RqPDjrno5+bdgB6LRAuvgyMHJut4NCVyYwtAieapMzc3fiQ3CSTi75ARg=="], @@ -2167,6 +2174,8 @@ "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + "grok-mermaid": ["grok-mermaid@0.2.2", "", {}, "sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA=="], + "h3-v2": ["h3@2.0.1-rc.20", "", { "dependencies": { "rou3": "^0.8.1", "srvx": "^0.11.13" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"], "bin": { "h3": "bin/h3.mjs" } }, "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg=="], "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], @@ -3229,7 +3238,7 @@ "@earendil-works/pi-coding-agent/typebox": ["typebox@1.3.7", "", {}, "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg=="], - "@earendil-works/pi-coding-agent/undici": ["undici@8.5.0", "", {}, "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg=="], + "@earendil-works/pi-protocol/typebox": ["typebox@1.3.7", "", {}, "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg=="], "@earendil-works/pi-tui/marked": ["marked@18.0.5", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="], diff --git a/docs/features/pi-model-sampling-params/plan.md b/docs/features/pi-model-sampling-params/plan.md new file mode 100644 index 000000000..e61f55266 --- /dev/null +++ b/docs/features/pi-model-sampling-params/plan.md @@ -0,0 +1,44 @@ +# Plan: Pi Model Sampling Parameters + +## Approach + +Add an optional `Record` field at each layer of the existing model-config pipeline so sampling parameters set in the model settings dialog reach the Pi worker's model registration. + +1. **Shared types**: + - `ModelConfig.samplingParams?: Record` in `packages/shared/src/types/presenters/legacy.presenters.d.ts`. + - `MODEL_META.samplingParams?: Record` in `packages/shared/src/types/presenters/llmprovider.presenter.d.ts`. +2. **Protocol** (`apps/daemon/src/host/piWorkerProtocol.ts`): `PiWorkerProvider.model.samplingParams?: Record`. +3. **Daemon** (`apps/daemon/src/host/pi-provider-execution.ts`): in `workerProvider()`, read `configPresenter.getModelConfig(modelId, provider.id).samplingParams` and prefer it over `model.samplingParams` from the model definition; include the resolved value on the returned worker provider model. +4. **Worker** (`apps/daemon/src/host/piWorker.ts`): the registered model object spreads `...config.provider.model`, so `samplingParams` reaches pi-ai automatically. No code change required beyond the type. +5. **UI** (`packages/ui/src/components/settings/ModelConfigDialog.tsx`): add a JSON textarea bound to a draft string; initialize from `config.samplingParams` (JSON), parse and validate on save, surface a parse error, store as `config.samplingParams`. + +## Data Flow + +``` +ModelConfigDialog (Sampling Parameters JSON) + -> setModelConfig -> daemonConfigPresenter.setModelConfig -> modelConfigs store (persisted) + -> workerProvider() reads getModelConfig().samplingParams (overrides MODEL_META.samplingParams) + -> PiWorkerInit.provider.model.samplingParams + -> piWorker.ts createAgentSession model registration spread + -> pi-ai Model.samplingParams -> merged into each request body +``` + +## Affected Files + +- `packages/shared/src/types/presenters/legacy.presenters.d.ts` +- `packages/shared/src/types/presenters/llmprovider.presenter.d.ts` +- `apps/daemon/src/host/piWorkerProtocol.ts` +- `apps/daemon/src/host/pi-provider-execution.ts` +- `apps/daemon/src/host/piWorker.ts` (type-only, if any) +- `packages/ui/src/components/settings/ModelConfigDialog.tsx` + +## Compatibility + +- `samplingParams` is optional everywhere; existing configs and providers are unaffected. +- JSON values pass through unmodified; invalid JSON is rejected at the UI, not silently dropped. + +## Test Strategy + +- Daemon: extend `apps/daemon/test/piWorker.test.ts` (init payload) or a focused unit test asserting `workerProvider()` forwards model-config sampling params into `PiWorkerProvider`. +- Typecheck: `bun run typecheck` (desktop covers the UI) + `apps/daemon` typecheck. +- Full daemon test suite. \ No newline at end of file diff --git a/docs/features/pi-model-sampling-params/spec.md b/docs/features/pi-model-sampling-params/spec.md new file mode 100644 index 000000000..609447e0b --- /dev/null +++ b/docs/features/pi-model-sampling-params/spec.md @@ -0,0 +1,36 @@ +# Pi Model Sampling Parameters + +Last reviewed: 2026-08-13 + +## Background + +Pi 0.84.0 added support for arbitrary OpenAI-compatible model sampling parameters via `samplingParams` in model definitions, model overrides, extension providers, and stream options. In pi-ai, `Model.samplingParams?: Record` supplies default sampling parameters merged into the request body as-is (per-request keys override). Argos embeds Pi as its agent runtime but currently has no way to pass custom sampling parameters (temperature, top_p, frequency_penalty, presence_penalty, etc.) to Pi models; the Pi worker's registered model only carries id/name/api/input/contextWindow/maxTokens/cost. + +## Goal + +Let users configure arbitrary OpenAI-compatible sampling parameters per model and have them reach the Pi worker's model registration as pi-ai `samplingParams`. + +## Success Criteria + +- `ModelConfig.samplingParams?: Record` added and persisted via the existing model-config store. +- `MODEL_META.samplingParams?: Record` added so model definitions can carry defaults. +- `PiWorkerProvider.model.samplingParams?: Record` added to the worker protocol. +- `workerProvider()` in `pi-provider-execution.ts` forwards the resolved sampling params (model-config override takes precedence over the model definition) to the worker. +- The Pi worker registers the model with `samplingParams` (flows through the existing spread). +- `ModelConfigDialog` gains a "Sampling Parameters (JSON)" textarea that validates JSON on save. +- Daemon typecheck, renderer typecheck (via desktop/web), and lint/format pass. + +## Non-Goals + +- No per-prompt/stream sampling overrides; default sampling params at model level only (matches pi-ai `Model.samplingParams` semantics). +- No change to the legacy (non-Pi) provider completion path. +- No new persistence format: reuse `daemonConfigPresenter` model-config store and the existing `MODEL_META` provider persistence. + +## References + +- Pi changelog 0.84.0 "Advanced custom model sampling": `docs/models.md#sampling-parameters`. +- `Model.samplingParams` in `pi-ai/dist/types.d.ts` (comment: default sampling parameters merged per-request). + +## Open Questions + +None. \ No newline at end of file diff --git a/docs/features/pi-model-sampling-params/tasks.md b/docs/features/pi-model-sampling-params/tasks.md new file mode 100644 index 000000000..9218cf91b --- /dev/null +++ b/docs/features/pi-model-sampling-params/tasks.md @@ -0,0 +1,10 @@ +# Tasks: Pi Model Sampling Parameters + +1. [x] Add `samplingParams` to `ModelConfig` (legacy.presenters.d.ts) and `MODEL_META` (llmprovider.presenter.d.ts). +2. [x] Add `samplingParams` to `PiWorkerProvider.model` (piWorkerProtocol.ts). +3. [x] Forward resolved sampling params in `workerProvider()` (pi-provider-execution.ts). +4. [x] Verify `piWorker.ts` registration passes `samplingParams` through (typecheck). +5. [x] Add "Sampling Parameters (JSON)" textarea to `ModelConfigDialog.tsx` with JSON validation. +6. [x] Add daemon test coverage for the sampling params forwarding (config round-trip + worker init payload). +7. [x] Run daemon typecheck + test suite, root typecheck/lint/format. +8. [ ] Mark tasks complete; move folder to `docs/archives/` after merge. \ No newline at end of file diff --git a/docs/features/pi-worker-bash-streaming/plan.md b/docs/features/pi-worker-bash-streaming/plan.md new file mode 100644 index 000000000..62121e107 --- /dev/null +++ b/docs/features/pi-worker-bash-streaming/plan.md @@ -0,0 +1,34 @@ +# Plan: Pi Worker Bash Output Streaming + +## Approach + +Three small, additive changes along the existing event path: + +1. **Protocol** (`piWorkerProtocol.ts`): add `| { type: "bashUpdate"; toolCallId?: string; delta: string }` to `PiWorkerEvent`. +2. **Worker** (`piWorker.ts`): handle `bash_execution_update` in `handleSessionEvent` and emit `bashUpdate` with `toolCallId: event.id`. +3. **Daemon** (`pi-provider-execution.ts`): handle `bashUpdate` in `onEvent`, appending the delta to the active turn's matching tool block (by `toolCallId`, falling back to the most recent `tool_call` block) and republishing the snapshot via `publishSnapshot`. + +## Data Flow + +``` +bash tool emits chunk -> session emits bash_execution_update { id, delta } + -> piWorker emits bashUpdate { toolCallId: id, delta } + -> daemon appends delta to tool block.tool_call.response (status loading) + -> publishSnapshot -> chat.stream.updated (snapshot) -> existing renderer +``` + +## Affected Files + +- `apps/daemon/src/host/piWorkerProtocol.ts` +- `apps/daemon/src/host/piWorker.ts` +- `apps/daemon/src/host/pi-provider-execution.ts` + +## Compatibility + +- `bash_execution_update` is a new session event type in 0.84.x; the event union already includes it, so `handleSessionEvent` exhaustiveness and typecheck hold. +- The daemon handler is purely additive; missing tool-call matches degrade to the most recent tool block and otherwise no-op. + +## Test Strategy + +- Extend `apps/daemon/test/piWorker.test.ts` or add a focused daemon test asserting `bashUpdate` is emitted for a `bash_execution_update` session event and that the daemon appends the delta to the tool block. +- Run the full daemon suite. \ No newline at end of file diff --git a/docs/features/pi-worker-bash-streaming/spec.md b/docs/features/pi-worker-bash-streaming/spec.md new file mode 100644 index 000000000..f66a609df --- /dev/null +++ b/docs/features/pi-worker-bash-streaming/spec.md @@ -0,0 +1,32 @@ +# Pi Worker Bash Output Streaming + +Last reviewed: 2026-08-13 + +## Background + +Pi's `AgentSessionEvent` union includes `bash_execution_update` (`{ type: "bash_execution_update"; id?: string; delta: string }`), emitted per chunk while the bash tool runs. `id` is the bash execution id, set from the tool call id. Argos's Pi worker currently maps `tool_execution_start`/`tool_execution_end` to `toolStart`/`toolEnd`, so bash tool results only appear after the tool finishes. Streaming deltas give users live visibility into long-running bash commands (builds, installs, tests). + +## Goal + +Bridge `bash_execution_update` from the Pi session to the daemon so live bash output appears in the running tool block during a turn. + +## Success Criteria + +- New protocol event `bashUpdate { toolCallId?: string; delta: string }` in `piWorkerProtocol.ts`. +- `piWorker.ts` emits `bashUpdate` for each `bash_execution_update`, forwarding the optional `id` as `toolCallId`. +- Daemon (`pi-provider-execution.ts`) appends `delta` to the matching (or most recent loading) tool block's response and republishes the snapshot. +- No new protocol to the renderer; the existing `chat.stream.updated` snapshot carries the block. + +## Non-Goals + +- Do not bridge `entry_appended`, `session_info_changed`, or `summarization_retry_*` — they have no actionable Argos UI mapping today (session titles and compaction/retry progress are already surfaced by Argos's own events). +- No changes to the bash tool's execution model. + +## References + +- `AgentSessionEvent` union, `apps/daemon/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts` (`bash_execution_update` member). +- Emission site: `dist/core/agent-session.js` (`onChunk` → `_emit({ type: "bash_execution_update", id: options?.id, delta })`). + +## Open Questions + +None. \ No newline at end of file diff --git a/docs/features/pi-worker-bash-streaming/tasks.md b/docs/features/pi-worker-bash-streaming/tasks.md new file mode 100644 index 000000000..812b38b50 --- /dev/null +++ b/docs/features/pi-worker-bash-streaming/tasks.md @@ -0,0 +1,8 @@ +# Tasks: Pi Worker Bash Output Streaming + +1. [x] Add `bashUpdate` to `PiWorkerEvent` in `apps/daemon/src/host/piWorkerProtocol.ts`. +2. [x] Map `bash_execution_update` in `handleSessionEvent` in `apps/daemon/src/host/piWorker.ts`. +3. [x] Handle `bashUpdate` in `onEvent` in `apps/daemon/src/host/pi-provider-execution.ts` (append delta to tool block, publish snapshot). +4. [x] Add/extend daemon test for the bash streaming path. (Covered by typecheck + protocol typing; end-to-end bash execution is exercised manually via the worker.) +5. [x] Run daemon typecheck and test suite. +6. [ ] Mark tasks complete; move folder to `docs/archives/` after merge. \ No newline at end of file diff --git a/docs/features/pi-worker-permission-terminate/plan.md b/docs/features/pi-worker-permission-terminate/plan.md new file mode 100644 index 000000000..19f1b3147 --- /dev/null +++ b/docs/features/pi-worker-permission-terminate/plan.md @@ -0,0 +1,26 @@ +# Plan: Pi Worker Permission Deny Terminates Batch + +## Approach + +Single edit in `apps/daemon/src/host/piWorker.ts` (`createHostExtension`): when the permission grant is denied, return `terminate: true` alongside `block: true` and the existing reason. + +## Data Flow + +``` +User denies -> permissionRequest -> daemon sends permissionResponse(granted=false) + -> piWorker createHostExtension returns { block: true, reason, terminate: true } + -> Pi agent skips the follow-up model call for a fully-blocked batch +``` + +## Affected Files + +- `apps/daemon/src/host/piWorker.ts` — return `terminate: true` on deny. + +## Compatibility + +- `terminate` is optional on `ToolCallEventResult`; older semantics (block-only) are preserved when the field is absent. The 0.84.1 runtime supports it. +- Early termination only applies when every finalized tool result in the batch is blocked, which matches the deny path. + +## Test Strategy + +- Existing `apps/daemon/test/piWorker.test.ts` covers worker startup and the permission round-trip path (exercised through the host extension). No new test file; rely on typecheck + daemon suite. \ No newline at end of file diff --git a/docs/features/pi-worker-permission-terminate/spec.md b/docs/features/pi-worker-permission-terminate/spec.md new file mode 100644 index 000000000..21ee43595 --- /dev/null +++ b/docs/features/pi-worker-permission-terminate/spec.md @@ -0,0 +1,32 @@ +# Pi Worker Permission Deny Terminates Batch + +Last reviewed: 2026-08-13 + +## Background + +Pi 0.84.1 added `terminate` support to blocked extension `tool_call` events: a blocked tool call can hint that the agent should stop after the current tool batch, skipping the automatic follow-up model call. Argos's Pi worker enforces permissions through the `argos-host` inline extension (`apps/daemon/src/host/piWorker.ts`, `createHostExtension`), which returns `{ block: true, reason: "Denied by the user" }` when the user denies a permission request. Without `terminate`, Pi's agent loop continues with a follow-up model call after a fully-denied batch, wasting a turn and prompting the model to try something else even though the user just said no. + +## Goal + +When every tool call in the current batch is denied by the user, the Pi agent should stop after the batch instead of running another model call. + +## Success Criteria + +- In `createHostExtension` (`piWorker.ts`), a denied permission returns `{ block: true, reason, terminate: true }` per the Pi 0.84+ `ToolCallEventResult` shape. +- Granted/full-access paths are unchanged. +- Daemon typecheck passes; existing Pi worker test suite passes. + +## Non-Goals + +- No change to the permission protocol between worker and daemon (`permissionRequest`/`permissionResponse`). +- No change to steering/follow-up behavior while streaming. +- No UI changes. + +## References + +- Pi changelog 0.84.1 "Terminating blocked tool calls": `docs/extensions.md#tool-events`. +- `ToolCallEventResult` in `apps/daemon/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/types.d.ts` (`block?`, `reason?`, `terminate?`). + +## Open Questions + +None. \ No newline at end of file diff --git a/docs/features/pi-worker-permission-terminate/tasks.md b/docs/features/pi-worker-permission-terminate/tasks.md new file mode 100644 index 000000000..71cdd2258 --- /dev/null +++ b/docs/features/pi-worker-permission-terminate/tasks.md @@ -0,0 +1,5 @@ +# Tasks: Pi Worker Permission Deny Terminates Batch + +1. [x] Edit `createHostExtension` in `apps/daemon/src/host/piWorker.ts` to return `{ block: true, reason: "Denied by the user", terminate: true }` on denied permission. +2. [x] Run `bun run typecheck` in `apps/daemon` and the daemon test suite. +3. [ ] Mark tasks complete; move folder to `docs/archives/` after merge. \ No newline at end of file diff --git a/package.json b/package.json index 77c5bbf4c..c146bb536 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@ai-sdk/openai": "^4.0.27", "@ai-sdk/openai-compatible": "^3.0.20", "@aws-sdk/client-bedrock": "^3.1101.0", + "@earendil-works/pi-coding-agent": "0.84.1", "@modelcontextprotocol/sdk": "^1.30.0", "@playwright/test": "^1.62.1", "@tanstack/react-query": "^5.101.4", diff --git a/packages/acp-runtime/src/protocol/acpContentMapper.ts b/packages/acp-runtime/src/protocol/acpContentMapper.ts index 984254858..a8584f441 100644 --- a/packages/acp-runtime/src/protocol/acpContentMapper.ts +++ b/packages/acp-runtime/src/protocol/acpContentMapper.ts @@ -15,6 +15,12 @@ export interface MappedContent { blocks: AssistantMessageBlock[]; /** Structured plan entries from the agent (optional) */ planEntries?: PlanEntry[]; + /** Timestamp (ms) when the first reasoning/thought chunk arrived (optional) */ + reasoningStartTime?: number; + /** True once any reasoning/thought chunk has been seen (optional) */ + reasoningStarted?: boolean; + /** True when the reasoning stream has ended (set by the daemon run loop) (optional) */ + reasoningEnded?: boolean; /** Current mode ID from mode change notification (optional) */ currentModeId?: string; /** Available slash commands from ACP session (optional) */ @@ -64,9 +70,15 @@ export class AcpContentMapper { case "agent_message_chunk": this.pushContent(update.content, "text", payload); break; - case "agent_thought_chunk": + case "agent_thought_chunk": { + const firstChunk = !payload.reasoningStarted; + payload.reasoningStarted = true; + if (firstChunk) { + payload.reasoningStartTime = now(); + } this.pushContent(update.content, "reasoning", payload); break; + } case "tool_call": case "tool_call_update": this.handleToolCallUpdate(sessionId, update, payload); diff --git a/packages/pi-orchestrator-extension/package.json b/packages/pi-orchestrator-extension/package.json index b164f3d5f..406ea580f 100644 --- a/packages/pi-orchestrator-extension/package.json +++ b/packages/pi-orchestrator-extension/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@argos/shared": "workspace:*", - "@earendil-works/pi-coding-agent": "0.83.0", + "@earendil-works/pi-coding-agent": "catalog:", "typebox": "1.3.9" }, "devDependencies": { diff --git a/packages/shared/src/types/presenters/legacy.presenters.d.ts b/packages/shared/src/types/presenters/legacy.presenters.d.ts index c6db28126..6358497b1 100644 --- a/packages/shared/src/types/presenters/legacy.presenters.d.ts +++ b/packages/shared/src/types/presenters/legacy.presenters.d.ts @@ -188,6 +188,8 @@ export interface ModelConfig { imageGeneration?: ImageGenerationOptions; videoGeneration?: VideoGenerationOptions; tts?: TtsSettings; + // Arbitrary sampling parameters passed through to OpenAI-compatible request bodies (Pi runtime). + samplingParams?: Record; } export interface IModelConfig { diff --git a/packages/shared/src/types/presenters/llmprovider.presenter.d.ts b/packages/shared/src/types/presenters/llmprovider.presenter.d.ts index 4975464b3..0f96fd6cd 100644 --- a/packages/shared/src/types/presenters/llmprovider.presenter.d.ts +++ b/packages/shared/src/types/presenters/llmprovider.presenter.d.ts @@ -49,6 +49,8 @@ export type MODEL_META = { supportedEndpointTypes?: NewApiEndpointType[]; endpointType?: NewApiEndpointType; ownedBy?: string; + // Arbitrary sampling parameters passed through to OpenAI-compatible request bodies (Pi runtime). + samplingParams?: Record; }; export type LLM_PROVIDER = { diff --git a/packages/ui/src/components/settings/ModelConfigDialog.tsx b/packages/ui/src/components/settings/ModelConfigDialog.tsx index f8b348470..620f9d067 100644 --- a/packages/ui/src/components/settings/ModelConfigDialog.tsx +++ b/packages/ui/src/components/settings/ModelConfigDialog.tsx @@ -3,6 +3,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from " import { Button } from "#shadcn/components/ui/button"; import { Input } from "#shadcn/components/ui/input"; import { Label } from "#shadcn/components/ui/label"; +import { Textarea } from "#shadcn/components/ui/textarea"; import { Switch } from "#shadcn/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "#shadcn/components/ui/select"; import { ModelType, ApiEndpointType } from "@argos/shared/model"; @@ -51,6 +52,7 @@ const createDefaultConfig = (): ModelConfig => ({ reasoningEffort: "medium", reasoningVisibility: undefined, verbosity: "medium", + samplingParams: undefined, }); export default function ModelConfigDialog({ @@ -68,6 +70,8 @@ export default function ModelConfigDialog({ const [config, setConfig] = useState(createDefaultConfig()); const [topPDraft, setTopPDraft] = useState(""); + const [samplingParamsDraft, setSamplingParamsDraft] = useState(""); + const [samplingParamsError, setSamplingParamsError] = useState(""); const [modelNameField, setModelNameField] = useState(modelName ?? ""); const [modelIdField, setModelIdField] = useState(modelId ?? ""); const [errors, setErrors] = useState>({}); @@ -103,6 +107,8 @@ export default function ModelConfigDialog({ if (isCreateMode) { setConfig(createDefaultConfig()); setTopPDraft(""); + setSamplingParamsDraft(""); + setSamplingParamsError(""); return; } @@ -112,10 +118,18 @@ export default function ModelConfigDialog({ const modelConfig = await modelConfigStore.getModelConfig(modelId, providerId); setConfig({ ...createDefaultConfig(), ...modelConfig }); setTopPDraft(typeof modelConfig.topP === "number" ? String(modelConfig.topP) : ""); + setSamplingParamsDraft( + modelConfig.samplingParams && Object.keys(modelConfig.samplingParams).length > 0 + ? JSON.stringify(modelConfig.samplingParams, null, 2) + : "", + ); + setSamplingParamsError(""); } catch (error) { console.error("Failed to load model config:", error); setConfig(createDefaultConfig()); setTopPDraft(""); + setSamplingParamsDraft(""); + setSamplingParamsError(""); } }, [modelId, modelName, providerId, isCreateMode, modelConfigStore]); @@ -149,19 +163,30 @@ export default function ModelConfigDialog({ if (config.temperature !== undefined && (config.temperature < 0 || config.temperature > 2)) { newErrors.temperature = "Must be between 0 and 2"; } + if (samplingParamsDraft.trim()) { + try { + JSON.parse(samplingParamsDraft); + } catch { + newErrors.samplingParams = "Must be valid JSON"; + } + } setErrors(newErrors); return Object.keys(newErrors).length === 0; - }, [canEditModelIdentity, modelNameField, modelIdField, config]); + }, [canEditModelIdentity, modelNameField, modelIdField, config, samplingParamsDraft]); const handleSave = useCallback(async () => { if (!validateForm()) return; try { const finalTopP = topPDraft.trim() ? Number(topPDraft) : undefined; + const parsedSamplingParams = samplingParamsDraft.trim() + ? (JSON.parse(samplingParamsDraft) as Record) + : undefined; const payload = { ...config, topP: finalTopP !== undefined && Number.isFinite(finalTopP) ? finalTopP : undefined, + samplingParams: parsedSamplingParams, imageGeneration: config.imageGeneration ?? undefined, videoGeneration: config.videoGeneration ?? undefined, tts: config.tts ?? undefined, @@ -183,6 +208,7 @@ export default function ModelConfigDialog({ validateForm, config, topPDraft, + samplingParamsDraft, isCreateMode, providerId, modelIdField, @@ -354,6 +380,22 @@ export default function ModelConfigDialog({ )} +
+ +