@humwick/runner 0.0.0-stage → 0.130.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +167 -0
- package/agent-bin/agent-relay-browser +10 -0
- package/agent-bin/humwick +10 -0
- package/hooks/fff-search-redirect/fff-search-redirect.cjs +161 -0
- package/hooks/fff-search-redirect/hooks.json +13 -0
- package/hooks/rg-r-guard/hooks.json +18 -0
- package/hooks/rg-r-guard/rg-r-guard.cjs +144 -0
- package/hooks/web-tools-redirect/hooks.json +13 -0
- package/hooks/web-tools-redirect/web-tools-redirect.cjs +84 -0
- package/node_modules/@humwick/offload/LICENSE +167 -0
- package/node_modules/@humwick/offload/README.md +57 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts +18 -0
- package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/async-runner.js +158 -0
- package/node_modules/@humwick/offload/dist/async-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts +25 -0
- package/node_modules/@humwick/offload/dist/envelope.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/envelope.js +80 -0
- package/node_modules/@humwick/offload/dist/envelope.js.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +10 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +59 -0
- package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +1 -0
- package/node_modules/@humwick/offload/dist/index.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/index.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/index.js +5 -0
- package/node_modules/@humwick/offload/dist/index.js.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +62 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js +295 -0
- package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +1 -0
- package/node_modules/@humwick/offload/dist/types.d.ts +111 -0
- package/node_modules/@humwick/offload/dist/types.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/types.js +176 -0
- package/node_modules/@humwick/offload/dist/types.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts +23 -0
- package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js +435 -0
- package/node_modules/@humwick/offload/dist/worker-pool.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +2 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js +27 -0
- package/node_modules/@humwick/offload/dist/worker-runtime.js.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts +5 -0
- package/node_modules/@humwick/offload/dist/worker.d.ts.map +1 -0
- package/node_modules/@humwick/offload/dist/worker.js +1 -0
- package/node_modules/@humwick/offload/dist/worker.js.map +1 -0
- package/node_modules/@humwick/offload/package.json +42 -0
- package/node_modules/@humwick/offload/src/async-runner.ts +170 -0
- package/node_modules/@humwick/offload/src/envelope.ts +92 -0
- package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +49 -0
- package/node_modules/@humwick/offload/src/index.ts +4 -0
- package/node_modules/@humwick/offload/src/subprocess-runner.ts +334 -0
- package/node_modules/@humwick/offload/src/types.ts +235 -0
- package/node_modules/@humwick/offload/src/worker-pool.ts +463 -0
- package/node_modules/@humwick/offload/src/worker-runtime.ts +32 -0
- package/node_modules/@humwick/offload/src/worker.ts +5 -0
- package/package.json +47 -4
- package/plugins/browser/skills/playwright-cli/SKILL.md +53 -0
- package/plugins/claude/.claude-plugin/plugin.json +8 -0
- package/plugins/claude/claude-statusline-probe.cjs +144 -0
- package/plugins/claude/hooks/gate-run-guard.provisioned.mjs +914 -0
- package/plugins/claude/hooks/gate-run-guard.sh +9 -0
- package/plugins/claude/hooks/gate-run-guard.ts +237 -0
- package/plugins/claude/hooks/hooks.json +158 -0
- package/plugins/claude/hooks/kill-guard.provisioned.mjs +7027 -0
- package/plugins/claude/hooks/kill-guard.sh +341 -0
- package/plugins/claude/hooks/kill-guard.ts +337 -0
- package/plugins/claude/hooks/permission-request.sh +22 -0
- package/plugins/claude/hooks/post-compact.sh +10 -0
- package/plugins/claude/hooks/post-tool-use.sh +8 -0
- package/plugins/claude/hooks/pre-compact.sh +12 -0
- package/plugins/claude/hooks/read-only-bash-guard.sh +587 -0
- package/plugins/claude/hooks/relay-status.sh +373 -0
- package/plugins/claude/hooks/session-end.sh +26 -0
- package/plugins/claude/hooks/session-start.sh +31 -0
- package/plugins/claude/hooks/shell-trust.ts +28 -0
- package/plugins/claude/hooks/stop-failure.sh +47 -0
- package/plugins/claude/hooks/stop.sh +48 -0
- package/plugins/claude/hooks/subagent-guard.cjs +26 -0
- package/plugins/claude/hooks/subagent-skill-policy.cjs +160 -0
- package/plugins/claude/hooks/subagent-start.sh +13 -0
- package/plugins/claude/hooks/subagent-stop.sh +19 -0
- package/plugins/claude/hooks/user-prompt-submit.sh +18 -0
- package/plugins/claude/hooks/wait-loop-guard.ts +120 -0
- package/plugins/claude/monitors/monitors.json +7 -0
- package/plugins/claude/monitors/relay-monitor.provisioned.mjs +786 -0
- package/plugins/claude/monitors/relay-monitor.ts +58 -0
- package/plugins/claude/skills/disconnect/SKILL.md +16 -0
- package/plugins/claude/skills/label/SKILL.md +23 -0
- package/plugins/claude/skills/message/SKILL.md +40 -0
- package/plugins/claude/skills/pair/SKILL.md +30 -0
- package/plugins/claude/skills/react/SKILL.md +18 -0
- package/plugins/claude/skills/read-message/SKILL.md +24 -0
- package/plugins/claude/skills/reply/SKILL.md +44 -0
- package/plugins/claude/skills/send-claimable/SKILL.md +39 -0
- package/plugins/claude/skills/slice-first/SKILL.md +32 -0
- package/plugins/claude/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/claude/skills/status/SKILL.md +16 -0
- package/plugins/claude/skills/tags/SKILL.md +25 -0
- package/plugins/claude/skills/task/SKILL.md +28 -0
- package/plugins/claude-output-style/.claude-plugin/plugin.json +5 -0
- package/plugins/claude-output-style/output-styles/agent-relay.md +5 -0
- package/plugins/codex/skills/disconnect/SKILL.md +15 -0
- package/plugins/codex/skills/guide/SKILL.md +15 -0
- package/plugins/codex/skills/label/SKILL.md +22 -0
- package/plugins/codex/skills/message/SKILL.md +40 -0
- package/plugins/codex/skills/pair/SKILL.md +29 -0
- package/plugins/codex/skills/react/SKILL.md +17 -0
- package/plugins/codex/skills/read-message/SKILL.md +23 -0
- package/plugins/codex/skills/reply/SKILL.md +44 -0
- package/plugins/codex/skills/send-claimable/SKILL.md +39 -0
- package/plugins/codex/skills/slice-first/SKILL.md +32 -0
- package/plugins/codex/skills/slice-first/references/plan-recipe.md +46 -0
- package/plugins/codex/skills/status/SKILL.md +15 -0
- package/plugins/codex/skills/tags/SKILL.md +24 -0
- package/plugins/codex/skills/task/SKILL.md +28 -0
- package/src/adapter.ts +1061 -0
- package/src/adapters/__fixtures__/codex-app-server.ts +47 -0
- package/src/adapters/claude-api.ts +243 -0
- package/src/adapters/claude-context-command.ts +16 -0
- package/src/adapters/claude-credentials.ts +22 -0
- package/src/adapters/claude-delivery-lanes.ts +28 -0
- package/src/adapters/claude-delivery.ts +166 -0
- package/src/adapters/claude-effort.ts +94 -0
- package/src/adapters/claude-exit.ts +290 -0
- package/src/adapters/claude-headless-mirror.ts +88 -0
- package/src/adapters/claude-headless-refusal.ts +51 -0
- package/src/adapters/claude-headless.ts +617 -0
- package/src/adapters/claude-monitor-delivery-plan.ts +317 -0
- package/src/adapters/claude-permission.ts +243 -0
- package/src/adapters/claude-prompt-gates.ts +269 -0
- package/src/adapters/claude-prompt-hook.ts +145 -0
- package/src/adapters/claude-session-capture.ts +293 -0
- package/src/adapters/claude-session-probe.ts +414 -0
- package/src/adapters/claude-shutdown.ts +190 -0
- package/src/adapters/claude-status-detectors.ts +426 -0
- package/src/adapters/claude-subagents.ts +93 -0
- package/src/adapters/claude-submit-confirmation.ts +287 -0
- package/src/adapters/claude-tmux.ts +548 -0
- package/src/adapters/claude-token-usage-capture.ts +253 -0
- package/src/adapters/claude-tool-policy.ts +165 -0
- package/src/adapters/claude-transcript-tail.ts +193 -0
- package/src/adapters/claude-transcript.ts +812 -0
- package/src/adapters/claude.ts +1097 -0
- package/src/adapters/codex-agent-message-capture.ts +189 -0
- package/src/adapters/codex-app-server.ts +70 -0
- package/src/adapters/codex-client.ts +362 -0
- package/src/adapters/codex-effective-config.ts +87 -0
- package/src/adapters/codex-launch-config.ts +153 -0
- package/src/adapters/codex-rollout-context.ts +272 -0
- package/src/adapters/codex-skill-discovery.ts +18 -0
- package/src/adapters/codex-token-usage-capture.ts +163 -0
- package/src/adapters/codex-tool-output.ts +53 -0
- package/src/adapters/codex-turn-outcome.ts +27 -0
- package/src/adapters/codex-version.ts +64 -0
- package/src/adapters/codex.ts +1442 -0
- package/src/adapters/incremental-file.ts +110 -0
- package/src/adapters/opencode-api.ts +71 -0
- package/src/adapters/opencode-context.ts +277 -0
- package/src/adapters/opencode-model-registry.ts +126 -0
- package/src/adapters/opencode-provider.ts +169 -0
- package/src/adapters/opencode-server-retry.ts +50 -0
- package/src/adapters/opencode-soft-steer.ts +302 -0
- package/src/adapters/opencode-spawn.ts +145 -0
- package/src/adapters/opencode-subscribe.ts +62 -0
- package/src/adapters/opencode-token-usage-capture.ts +187 -0
- package/src/adapters/opencode-tool-events.ts +302 -0
- package/src/adapters/opencode-tools-config.ts +17 -0
- package/src/adapters/opencode.ts +2264 -0
- package/src/adapters/project-shell.ts +80 -0
- package/src/adapters/provider-launch-config.ts +21 -0
- package/src/adapters/provider-quota-harvest.ts +127 -0
- package/src/adapters/service-access.ts +153 -0
- package/src/adapters/tmux-offload.ts +207 -0
- package/src/adapters/tool-summary.ts +214 -0
- package/src/agent-redact.ts +75 -0
- package/src/agent-runner/commands/dispatch.ts +185 -0
- package/src/agent-runner/commands/lifecycle.ts +84 -0
- package/src/agent-runner/commands/prompt.ts +284 -0
- package/src/agent-runner/credentials.ts +208 -0
- package/src/agent-runner/http-liveness.ts +85 -0
- package/src/agent-runner/lifecycle-milestones.ts +207 -0
- package/src/agent-runner/lifecycle-phase.ts +80 -0
- package/src/agent-runner/registration.ts +163 -0
- package/src/agent-runner/runner-context.ts +158 -0
- package/src/agent-runner/session-mirror.ts +278 -0
- package/src/agent-runner/task-claims.ts +116 -0
- package/src/agent-runner/token-usage-coverage.ts +54 -0
- package/src/agent-runner/turn-report.ts +269 -0
- package/src/api-budget-accounting.ts +91 -0
- package/src/area-memory.ts +255 -0
- package/src/artifact-path-materialize.ts +52 -0
- package/src/async-guard.ts +35 -0
- package/src/attachment-cache-sweep.ts +31 -0
- package/src/attachment-cache.ts +195 -0
- package/src/background-script-reconcile.ts +106 -0
- package/src/bounded-log.ts +59 -0
- package/src/bounded-spawn-sync.ts +18 -0
- package/src/busy-reconciler.ts +282 -0
- package/src/claim-tracker.ts +140 -0
- package/src/claude-settings.ts +167 -0
- package/src/claude-workspace-trust.ts +289 -0
- package/src/config.ts +547 -0
- package/src/context-boundary.ts +101 -0
- package/src/context-command.ts +151 -0
- package/src/context-probe-state.ts +6 -0
- package/src/continuation-archive.ts +179 -0
- package/src/control-server.ts +730 -0
- package/src/delivery-policy.ts +9 -0
- package/src/delivery-result.ts +24 -0
- package/src/effort-command.ts +125 -0
- package/src/empty-turn.ts +816 -0
- package/src/free-port.ts +25 -0
- package/src/fs-offload.ts +26 -0
- package/src/gate-run-config.ts +35 -0
- package/src/gate-run-lease-probe.ts +38 -0
- package/src/host-asset-plan.ts +450 -0
- package/src/host-claude-config-worker.ts +34 -0
- package/src/host-sources.ts +40 -0
- package/src/inbox-carrier.ts +7 -0
- package/src/index.ts +386 -0
- package/src/launch-assembly.ts +955 -0
- package/src/liveness.ts +105 -0
- package/src/logger.ts +108 -0
- package/src/mcp-outbox.ts +204 -0
- package/src/mcp-preflight.ts +86 -0
- package/src/mcp-source-selection.ts +54 -0
- package/src/mcp-tool-deferral.ts +35 -0
- package/src/mcp-vault-env.ts +25 -0
- package/src/message-body-config/config.ts +16 -0
- package/src/message-body-config.ts +1 -0
- package/src/mid-turn-delivery.ts +43 -0
- package/src/native-memory-lever.ts +329 -0
- package/src/native-self-resume.ts +244 -0
- package/src/outbox.ts +453 -0
- package/src/park-post.ts +17 -0
- package/src/pending-permission-store.ts +148 -0
- package/src/process-meta.ts +9 -0
- package/src/profile-home.ts +419 -0
- package/src/profile-projection.ts +411 -0
- package/src/project-hooks.ts +65 -0
- package/src/project-instructions.ts +119 -0
- package/src/provider-double.ts +59 -0
- package/src/provider-hold.ts +228 -0
- package/src/provider-home-marker.ts +3 -0
- package/src/provider-home-tombstone.ts +4 -0
- package/src/provider-input-queue.ts +134 -0
- package/src/provider-integration.ts +93 -0
- package/src/provider-processes.ts +17 -0
- package/src/provider-submit-failure.ts +15 -0
- package/src/providers.ts +35 -0
- package/src/provisioning.ts +703 -0
- package/src/question-decision.ts +18 -0
- package/src/quota.ts +22 -0
- package/src/rate-limit.ts +351 -0
- package/src/refused-prompt-store.ts +36 -0
- package/src/relay-injection-events.ts +161 -0
- package/src/relay-instructions.ts +107 -0
- package/src/relay-mcp-proxy.long-turn-e2e.ts +183 -0
- package/src/relay-mcp-proxy.ts +704 -0
- package/src/relay-mcp.ts +583 -0
- package/src/reply-obligation-cache.ts +211 -0
- package/src/response-capture-report.ts +111 -0
- package/src/response-routing.ts +6 -0
- package/src/runner-core.ts +2955 -0
- package/src/runner-exit.ts +12 -0
- package/src/runner-helpers.ts +582 -0
- package/src/runner-insights.ts +105 -0
- package/src/runner-mcp-proxy.ts +46 -0
- package/src/runner-spawn-config.ts +68 -0
- package/src/runner-stale-scratch-sweep.ts +21 -0
- package/src/runner.ts +9 -0
- package/src/runtime-token-file.ts +45 -0
- package/src/runtime-token-remint.ts +56 -0
- package/src/service-access.ts +88 -0
- package/src/session-destroy.ts +27 -0
- package/src/session-insights.ts +118 -0
- package/src/session-scratch.ts +296 -0
- package/src/spawn-input.ts +147 -0
- package/src/startup-config-fetch.ts +48 -0
- package/src/subreaper.ts +113 -0
- package/src/template-resolver.ts +29 -0
- package/src/test-run-dedup.ts +435 -0
- package/src/transcript-context.ts +99 -0
- package/src/transport-errors.ts +48 -0
- package/src/turn-opener.ts +55 -0
- package/src/turn-reconcile-event.ts +88 -0
- package/src/vault-proxy-hop.ts +99 -0
- package/src/version.ts +44 -0
- package/src/web-tool-call-proxy.ts +128 -0
- package/src/web-tools.ts +59 -0
- package/README.md +0 -3
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
import { errMessage, isRecord } from "@humwick/sdk";
|
|
3
|
+
import { logger } from "../logger";
|
|
4
|
+
import type { OpenCodeServerEvent } from "./opencode";
|
|
5
|
+
|
|
6
|
+
export async function subscribeOpenCodeEvents(request: () => Promise<Response>, signal: AbortSignal, onEvent: (event: OpenCodeServerEvent) => void, retryDelay: (ms: number, signal: AbortSignal) => Promise<unknown> = (ms, abort) => delay(ms, undefined, { signal: abort })): Promise<void> {
|
|
7
|
+
let failures = 0;
|
|
8
|
+
while (!signal.aborted) {
|
|
9
|
+
try {
|
|
10
|
+
await consumeEvents(request, signal, onEvent, () => { failures = 0; });
|
|
11
|
+
if (signal.aborted) return;
|
|
12
|
+
throw new Error("event stream ended");
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (signal.aborted) return;
|
|
15
|
+
const delayMs = Math.min(30_000, 1_000 * 2 ** Math.min(failures++, 5));
|
|
16
|
+
logger.error("opencode", `SSE subscription failed: ${errMessage(error)}; reconnecting in ${delayMs}ms`);
|
|
17
|
+
try {
|
|
18
|
+
await retryDelay(delayMs, signal);
|
|
19
|
+
} catch (delayError) {
|
|
20
|
+
if (signal.aborted) return;
|
|
21
|
+
throw delayError;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function consumeEvents(request: () => Promise<Response>, signal: AbortSignal, onEvent: (event: OpenCodeServerEvent) => void, onReceived: () => void): Promise<void> {
|
|
28
|
+
const response = await request();
|
|
29
|
+
if (!response.body) throw new Error("event stream has no body");
|
|
30
|
+
const reader = response.body.getReader();
|
|
31
|
+
const decoder = new TextDecoder();
|
|
32
|
+
let buffered = "";
|
|
33
|
+
try {
|
|
34
|
+
while (!signal.aborted) {
|
|
35
|
+
const read = await reader.read();
|
|
36
|
+
if (read.done) return;
|
|
37
|
+
buffered += decoder.decode(read.value, { stream: true });
|
|
38
|
+
const frames = buffered.split("\n\n");
|
|
39
|
+
buffered = frames.pop() ?? "";
|
|
40
|
+
for (const frame of frames) {
|
|
41
|
+
const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()).join("\n");
|
|
42
|
+
if (!data) continue;
|
|
43
|
+
let event: unknown;
|
|
44
|
+
try {
|
|
45
|
+
event = JSON.parse(data);
|
|
46
|
+
} catch {
|
|
47
|
+
continue; // malformed event is isolated; the stream remains usable
|
|
48
|
+
}
|
|
49
|
+
if (isRecord(event) && typeof event.type === "string") {
|
|
50
|
+
onReceived();
|
|
51
|
+
try {
|
|
52
|
+
onEvent({ type: event.type, ...(isRecord(event.properties) ? { properties: event.properties } : {}) });
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.error("opencode", `SSE event handler failed for ${event.type}: ${errMessage(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} finally {
|
|
60
|
+
reader.releaseLock();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// #2484 — per-turn OpenCode/Zai token-usage capture, reusing the #2482/#2485 spine seam
|
|
2
|
+
// (ProviderTokenUsageReporter + POST /api/agents/:id/token-usage, validated live via Claude's
|
|
3
|
+
// #2491 capture). Unlike Claude, OpenCode never hands the runner a transcript file to tail: the
|
|
4
|
+
// `message.updated` SSE door already carries a structured per-assistant-message `tokens` object
|
|
5
|
+
// once a message finalizes — `{ input, output, reasoning, cache: { read, write } }`, confirmed
|
|
6
|
+
// live against a real captured v1.18.21 session (see opencode-context.ts's #2355 note beside the
|
|
7
|
+
// same wire shape). So there is no incremental-file tail here: this module is a pure decoder from
|
|
8
|
+
// that object to the spine's required breakdown, called straight from the SSE handler.
|
|
9
|
+
//
|
|
10
|
+
// Dedupe: the door fires more than once per message as it streams toward its final total (#2427).
|
|
11
|
+
// Keying on the message id makes every echo collapse onto the SAME spine row via the dedupe_key
|
|
12
|
+
// upsert (`created_at >= existing` replaces) — the row simply refines towards the message's true
|
|
13
|
+
// total rather than posting a new one per frame, exactly like Claude's uuid-keyed entries collapse
|
|
14
|
+
// repeated capture passes onto one row.
|
|
15
|
+
import { OPENAI_API_MODEL, isRecord, stringValue } from "@humwick/sdk";
|
|
16
|
+
import type { ProviderTokenUsageRecord, ProviderTokenUsageReporter } from "../adapter";
|
|
17
|
+
|
|
18
|
+
export type OpenCodeTokenUsageSpineRecord = ProviderTokenUsageRecord;
|
|
19
|
+
|
|
20
|
+
function numberOrZero(value: unknown): number {
|
|
21
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decompose OpenCode's per-message `tokens` object into the spine's required input/output/cache
|
|
26
|
+
* breakdown. Returns `undefined` when the shape is not the recognised record (absent, a bare
|
|
27
|
+
* number, or every leaf zero/unreadable) — never fabricating a breakdown it cannot actually read,
|
|
28
|
+
* mirroring the "only claim what was understood" rule `openCodeCompletion` already applies to this
|
|
29
|
+
* same wire family.
|
|
30
|
+
*/
|
|
31
|
+
export function openCodeTokenUsageRecord(input: {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
provider: string;
|
|
34
|
+
model: string;
|
|
35
|
+
messageId: string;
|
|
36
|
+
tokens: unknown;
|
|
37
|
+
createdAt?: number;
|
|
38
|
+
/** #3351 — the message is final, so unreadable counts mean unknown spend, not "not yet". */
|
|
39
|
+
finished?: boolean;
|
|
40
|
+
}): OpenCodeTokenUsageSpineRecord | undefined {
|
|
41
|
+
const unknown = (): OpenCodeTokenUsageSpineRecord | undefined => input.finished ? {
|
|
42
|
+
sessionId: input.sessionId, provider: input.provider, model: input.model,
|
|
43
|
+
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0,
|
|
44
|
+
dedupeKey: input.messageId, createdAt: input.createdAt ?? Date.now(), usageStatus: "unknown",
|
|
45
|
+
} : undefined;
|
|
46
|
+
if (!isRecord(input.tokens)) return unknown();
|
|
47
|
+
const cache = isRecord(input.tokens.cache) ? input.tokens.cache : undefined;
|
|
48
|
+
if (input.model === OPENAI_API_MODEL && ![input.tokens.input, input.tokens.output, cache?.read, cache?.write, input.tokens.reasoning === undefined ? 0 : input.tokens.reasoning]
|
|
49
|
+
.every(n => typeof n === "number" && Number.isSafeInteger(n) && n >= 0)) return unknown();
|
|
50
|
+
const inputTokens = numberOrZero(input.tokens.input);
|
|
51
|
+
const outputTokens = numberOrZero(input.tokens.output);
|
|
52
|
+
const reasoningOutputTokens = numberOrZero(input.tokens.reasoning);
|
|
53
|
+
const cacheReadTokens = numberOrZero(cache?.read);
|
|
54
|
+
const cacheWriteTokens = numberOrZero(cache?.write);
|
|
55
|
+
// A recognised-but-entirely-empty object carries no signal — the same no-signal rule
|
|
56
|
+
// `openCodeContextState` applies to a `tokensUsed:0` reading of this identical shape (#2355
|
|
57
|
+
// review MED-1: a genuinely engaged assistant message always has input > 0).
|
|
58
|
+
if (inputTokens === 0 && outputTokens === 0 && reasoningOutputTokens === 0 && cacheReadTokens === 0 && cacheWriteTokens === 0) return unknown();
|
|
59
|
+
return {
|
|
60
|
+
sessionId: input.sessionId,
|
|
61
|
+
provider: input.provider,
|
|
62
|
+
model: input.model,
|
|
63
|
+
inputTokens,
|
|
64
|
+
outputTokens,
|
|
65
|
+
cacheReadTokens,
|
|
66
|
+
cacheWriteTokens,
|
|
67
|
+
...(reasoningOutputTokens > 0 ? { reasoningOutputTokens } : {}),
|
|
68
|
+
dedupeKey: input.messageId,
|
|
69
|
+
createdAt: input.createdAt ?? Date.now(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* #2484 round 1 review (F6) — the message's OWN observed usage time: `time.completed` once
|
|
75
|
+
* finalized, else `time.created` (present on every AssistantMessage, per opencode-context.ts's
|
|
76
|
+
* #2355 note on this same wire family). Reading this instead of `Date.now()` at report time
|
|
77
|
+
* removes the equal-millisecond upsert race for the TERMINAL echo specifically: every
|
|
78
|
+
* streaming echo of a still-open message shares the SAME `time.created`, and the terminal
|
|
79
|
+
* echo's own `time.completed` is always `>=` it — so the spine's `created_at >= existing`
|
|
80
|
+
* upsert can never let a stale reprocessing of an earlier, still-open echo clobber the
|
|
81
|
+
* terminal, complete read the way a receipt-time `Date.now()` could. (Round 2 review, LOW-1:
|
|
82
|
+
* two PRE-completion echoes of the same still-open message still share that same
|
|
83
|
+
* `time.created` and so are not individually ordered against each other by this alone — the
|
|
84
|
+
* debounced poster's own last-write-wins `schedule()` is what settles that case, since only
|
|
85
|
+
* the most recently scheduled record ever reaches `flush()`.)
|
|
86
|
+
*/
|
|
87
|
+
export function openCodeMessageObservedAt(info: unknown): number | undefined {
|
|
88
|
+
if (!isRecord(info) || !isRecord(info.time)) return undefined;
|
|
89
|
+
const completed = info.time.completed;
|
|
90
|
+
if (typeof completed === "number" && Number.isFinite(completed)) return completed;
|
|
91
|
+
const created = info.time.created;
|
|
92
|
+
return typeof created === "number" && Number.isFinite(created) ? created : undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Terminal messages can carry billed usage even when the turn errored or was aborted. */
|
|
96
|
+
export function openCodeMessageFinished(info: unknown): boolean {
|
|
97
|
+
return isRecord(info) && ((isRecord(info.time) && typeof info.time.completed === "number") || isRecord(info.error));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* #2484 round 1 review (F5) — the model the message was actually generated with, read off its own
|
|
102
|
+
* `providerID`/`modelID` (the same shape `openCodeModelDescriptor` sends on the request) rather
|
|
103
|
+
* than the session's currently-configured model, which can have moved on (a mid-session
|
|
104
|
+
* model/provider swap, a #2397 retry re-roll) by the time this echo is processed.
|
|
105
|
+
*/
|
|
106
|
+
export function openCodeMessageModel(info: unknown): string | undefined {
|
|
107
|
+
if (!isRecord(info)) return undefined;
|
|
108
|
+
const providerID = stringValue(info.providerID);
|
|
109
|
+
const modelID = stringValue(info.modelID);
|
|
110
|
+
return providerID && modelID ? `${providerID}/${modelID}` : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const TOKEN_USAGE_DEBOUNCE_MS = 300;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* #2484 round 1 review (F2/F3) — the `message.updated` door fires repeatedly as one message
|
|
117
|
+
* streams to completion; posting on every echo means an HTTP POST + SQLite write per frame,
|
|
118
|
+
* fleet-wide, for a value the very next echo immediately supersedes. Trailing-debounce per
|
|
119
|
+
* message id: each new echo replaces the pending record and resets the timer, so a whole
|
|
120
|
+
* streaming burst collapses to ONE post carrying the terminal value. `flush`/`flushAll` let the
|
|
121
|
+
* adapter force an immediate post at a real turn boundary (turn close, shutdown) rather than
|
|
122
|
+
* risk losing the last burst's report to a timer that never gets to fire.
|
|
123
|
+
*/
|
|
124
|
+
export class OpenCodeTokenUsagePoster {
|
|
125
|
+
private readonly pending = new Map<string, { record: OpenCodeTokenUsageSpineRecord; timer: ReturnType<typeof setTimeout> }>();
|
|
126
|
+
|
|
127
|
+
constructor(
|
|
128
|
+
private readonly reporter: () => ProviderTokenUsageReporter | undefined,
|
|
129
|
+
private readonly debounceMs = TOKEN_USAGE_DEBOUNCE_MS,
|
|
130
|
+
) {}
|
|
131
|
+
|
|
132
|
+
schedule(record: OpenCodeTokenUsageSpineRecord): void {
|
|
133
|
+
const prior = this.pending.get(record.dedupeKey);
|
|
134
|
+
if (prior) clearTimeout(prior.timer);
|
|
135
|
+
const timer = setTimeout(() => this.flush(record.dedupeKey), this.debounceMs);
|
|
136
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
137
|
+
this.pending.set(record.dedupeKey, { record, timer });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// #2484 round 2 review (MED-1) — returns the in-flight delivery so a teardown caller can
|
|
141
|
+
// actually await it (previously fire-and-forget: a caller that returned right after calling
|
|
142
|
+
// this had no guarantee the POST had even started, let alone landed — the repo's
|
|
143
|
+
// persisted-is-not-delivered shape). A live turn-boundary call (closeSuppressedTurn/
|
|
144
|
+
// closeEventTurn/failTurn) still doesn't need to await it: the promise's own `.catch` already
|
|
145
|
+
// routes failures to `reporter.reportFailure`, so nothing is lost by not awaiting there.
|
|
146
|
+
flush(dedupeKey: string): Promise<void> {
|
|
147
|
+
const entry = this.pending.get(dedupeKey);
|
|
148
|
+
if (!entry) return Promise.resolve();
|
|
149
|
+
this.pending.delete(dedupeKey);
|
|
150
|
+
clearTimeout(entry.timer);
|
|
151
|
+
const reporter = this.reporter();
|
|
152
|
+
if (!reporter) return Promise.resolve();
|
|
153
|
+
return reporter
|
|
154
|
+
.report(entry.record)
|
|
155
|
+
.then(() => reporter.debug(`token-usage capture: posted ${entry.record.dedupeKey}`))
|
|
156
|
+
.catch((error) => reporter.reportFailure(error));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
flushAll(): Promise<void> {
|
|
160
|
+
return Promise.all([...this.pending.keys()].map((dedupeKey) => this.flush(dedupeKey))).then(() => undefined);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Decode the `message.updated` door's `info` into a spine record and schedule it through
|
|
166
|
+
* `poster`. A no-op whenever the session/model is unknown or {@link openCodeTokenUsageRecord}
|
|
167
|
+
* couldn't decompose the tokens object — never fabricating a report from a shape it didn't
|
|
168
|
+
* understand, and never throwing when no reporter has been configured yet (the poster itself
|
|
169
|
+
* absorbs that at flush time).
|
|
170
|
+
*/
|
|
171
|
+
export function scheduleOpenCodeTokenUsage(
|
|
172
|
+
poster: OpenCodeTokenUsagePoster,
|
|
173
|
+
input: { provider: string; sessionId: string | undefined; model: string | undefined; messageId: string; tokens: unknown; createdAt: number | undefined; finished?: boolean },
|
|
174
|
+
): void {
|
|
175
|
+
if (!input.sessionId || !input.model) return;
|
|
176
|
+
const record = openCodeTokenUsageRecord({
|
|
177
|
+
sessionId: input.sessionId,
|
|
178
|
+
provider: input.provider,
|
|
179
|
+
model: input.model,
|
|
180
|
+
messageId: input.messageId,
|
|
181
|
+
tokens: input.tokens,
|
|
182
|
+
createdAt: input.createdAt,
|
|
183
|
+
finished: input.finished,
|
|
184
|
+
});
|
|
185
|
+
if (!record) return;
|
|
186
|
+
poster.schedule(record);
|
|
187
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2354 — decode OpenCode's `message.part.updated` tool parts (v1.18.21 `ToolPart`) into the
|
|
3
|
+
* provider-neutral tool-step shape the chat/timeline already renders (TraceView `ToolCallRow`).
|
|
4
|
+
*
|
|
5
|
+
* `opencode.ts` is near the module-size ceiling, so the wire-decoding lives here as a small,
|
|
6
|
+
* pure, separately-testable unit; the adapter keeps only the stateful dedup/ordering and the
|
|
7
|
+
* `sessionEventCb` emit. Modelled on the Codex adapter (stable item-id running/completed upserts),
|
|
8
|
+
* per the operator's "handle like we do in codex" instruction.
|
|
9
|
+
*
|
|
10
|
+
* ToolPart wire shape (pinned v1.18.21):
|
|
11
|
+
* { id, callID, tool, state: { status: "pending"|"running"|"completed"|"error", … } }
|
|
12
|
+
* - running: input, title, metadata
|
|
13
|
+
* - completed: output, title, metadata, time
|
|
14
|
+
* - error: error, metadata, and an `interrupted` flag when a running tool was aborted
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { isRecord, stringValue, type ToolCallCategory } from "@humwick/sdk";
|
|
18
|
+
import type { ProviderSessionEvent } from "../adapter";
|
|
19
|
+
import { absoluteToolPaths, clipToolSummary, oneLineText, redactAndCapParams, summarizeToolInput } from "./tool-summary";
|
|
20
|
+
|
|
21
|
+
/** The provider-neutral tool-step lifecycle status. `cancelled` is an interrupted-before-finished
|
|
22
|
+
* tool (ToolPart error state with `interrupted:true`), distinct from `failed` (ran and errored). */
|
|
23
|
+
export type OpenCodeToolStepStatus = "running" | "completed" | "failed" | "cancelled";
|
|
24
|
+
|
|
25
|
+
export interface OpenCodeToolStep {
|
|
26
|
+
/** Stable part id → session-step `stepId`, so running→completed upserts one row in place. */
|
|
27
|
+
stepId: string;
|
|
28
|
+
/** The raw OpenCode tool name (e.g. "bash", "read", an MCP tool) — short row label. */
|
|
29
|
+
label: string;
|
|
30
|
+
category: ToolCallCategory;
|
|
31
|
+
status: OpenCodeToolStepStatus;
|
|
32
|
+
/** Safe one-line summary (redacted + clipped) of what the tool is doing. */
|
|
33
|
+
body: string;
|
|
34
|
+
/** #2437 — redacted, size-capped tool input. Only present on the "running" state (the ToolPart
|
|
35
|
+
* wire shape drops `input` once the tool completes) — undefined here means "not captured on
|
|
36
|
+
* THIS decode", not "no params exist"; see `emitToolStep`'s stepId cache for the carry-forward
|
|
37
|
+
* onto the terminal state. */
|
|
38
|
+
params?: unknown;
|
|
39
|
+
/** #3411 — absolute paths a file tool reads or edits, from the raw input (running state only). */
|
|
40
|
+
paths?: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** OpenCode built-in tool name → the provider-independent category taxonomy (#1498). Anything
|
|
44
|
+
* not a known built-in (an MCP tool, a future built-in) falls to the mcp/generic default —
|
|
45
|
+
* never force-fit, so nothing is hidden from the trace. */
|
|
46
|
+
const OPENCODE_TOOL_CATEGORIES: Record<string, ToolCallCategory> = {
|
|
47
|
+
bash: "cli-bash",
|
|
48
|
+
edit: "edit",
|
|
49
|
+
write: "edit",
|
|
50
|
+
patch: "edit",
|
|
51
|
+
multiedit: "edit",
|
|
52
|
+
read: "read",
|
|
53
|
+
list: "read",
|
|
54
|
+
task: "agent-spawn",
|
|
55
|
+
todowrite: "plan",
|
|
56
|
+
todoread: "plan",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** #3411 — OpenCode's file tools; each names its file in `input.filePath`. */
|
|
60
|
+
const OPENCODE_FILE_TOOLS = new Set(["read", "edit", "write", "multiedit"]);
|
|
61
|
+
|
|
62
|
+
/** Extra input-field names OpenCode's built-in tools use that aren't in the shared candidate
|
|
63
|
+
* list — its file tools use camelCase `filePath` (already shared) and `todo`/`glob` shapes. */
|
|
64
|
+
const OPENCODE_INPUT_KEYS = ["filePath", "cwd"] as const;
|
|
65
|
+
|
|
66
|
+
export function openCodeToolCategory(tool: string): ToolCallCategory {
|
|
67
|
+
const known = OPENCODE_TOOL_CATEGORIES[tool.toLowerCase()];
|
|
68
|
+
if (known) return known;
|
|
69
|
+
// MCP tools surface through OpenCode with a namespaced name; the `__` convention (shared with
|
|
70
|
+
// Claude's `mcp__server__tool`) is the safe signal. Everything else is generic (#2354 rule:
|
|
71
|
+
// unknown tool → generic), so a mystery tool still renders rather than vanishing.
|
|
72
|
+
if (tool.includes("__") || tool.toLowerCase().startsWith("mcp")) return "mcp-tool-call";
|
|
73
|
+
return "generic";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Map the ToolPart state to the shared lifecycle status. `pending` (queued but not yet started)
|
|
78
|
+
* collapses to `running` — the trace shows one "in-flight" state, matching Codex, which emits a
|
|
79
|
+
* single running upsert. An `error` state splits: an interrupted tool is `cancelled` (we/the
|
|
80
|
+
* operator aborted it — not the model's fault), a non-interrupted error is `failed`. An
|
|
81
|
+
* unrecognised/missing status is treated as in-flight (`running`) rather than dropped.
|
|
82
|
+
*/
|
|
83
|
+
export function openCodeToolStatus(state: Record<string, unknown> | undefined): OpenCodeToolStepStatus {
|
|
84
|
+
const rawStatus = stringValue(state?.status);
|
|
85
|
+
if (rawStatus === "completed") return "completed";
|
|
86
|
+
if (rawStatus === "error") return state?.interrupted === true ? "cancelled" : "failed";
|
|
87
|
+
return "running";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Lifecycle rank for the adapter's terminal-lock/dedup: a terminal state (2) must never be
|
|
91
|
+
* regressed to running (1) by a duplicate/reordered update. Mirrors the server's
|
|
92
|
+
* `sessionStepStatusRank`. */
|
|
93
|
+
export function openCodeToolStatusRank(status: OpenCodeToolStepStatus): number {
|
|
94
|
+
return status === "running" ? 1 : 2;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The message the ToolPart error state carries, defensively (string, `{message}`, `{data.message}`,
|
|
98
|
+
* or a `{name}` fallback). */
|
|
99
|
+
function openCodeToolErrorText(error: unknown): string {
|
|
100
|
+
if (typeof error === "string") return oneLineText(error);
|
|
101
|
+
if (!isRecord(error)) return "";
|
|
102
|
+
const direct = oneLineText(error.message);
|
|
103
|
+
if (direct) return direct;
|
|
104
|
+
const nested = isRecord(error.data) ? oneLineText(error.data.message) : "";
|
|
105
|
+
if (nested) return nested;
|
|
106
|
+
return oneLineText(error.name);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The safe body summary for a tool step, in priority order: the input args' most meaningful field
|
|
111
|
+
* (present while running), then the server-supplied human `title`, then the error text (error
|
|
112
|
+
* state), then the tool name. Raw tool OUTPUT is deliberately never surfaced — it can be huge and
|
|
113
|
+
* unbounded (Codex applies the same rule), and `title` already names the result. Everything routes
|
|
114
|
+
* through `clipToolSummary`, so redaction can't be skipped.
|
|
115
|
+
*/
|
|
116
|
+
function openCodeToolBody(tool: string, state: Record<string, unknown> | undefined): string {
|
|
117
|
+
if (!state) return tool;
|
|
118
|
+
const input = isRecord(state.input) ? state.input : undefined;
|
|
119
|
+
const inputSummary = summarizeToolInput(input, OPENCODE_INPUT_KEYS);
|
|
120
|
+
if (inputSummary) return inputSummary;
|
|
121
|
+
const title = oneLineText(state.title);
|
|
122
|
+
if (title) return title;
|
|
123
|
+
const error = openCodeToolErrorText(state.error);
|
|
124
|
+
if (error) return error;
|
|
125
|
+
return tool;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Decode one `message.part.updated` part into a tool step, or `undefined` when the part is not a
|
|
130
|
+
* decodable tool part (wrong type, or missing the stable id/name we key the upsert on). Pure — the
|
|
131
|
+
* adapter owns the dedup/ordering state and the emit.
|
|
132
|
+
*/
|
|
133
|
+
export function decodeOpenCodeToolPart(part: unknown): OpenCodeToolStep | undefined {
|
|
134
|
+
if (!isRecord(part)) return undefined;
|
|
135
|
+
if (stringValue(part.type) !== "tool") return undefined;
|
|
136
|
+
const stepId = stringValue(part.id);
|
|
137
|
+
const tool = stringValue(part.tool);
|
|
138
|
+
if (!stepId || !tool) return undefined;
|
|
139
|
+
const state = isRecord(part.state) ? part.state : undefined;
|
|
140
|
+
const input = isRecord(state?.input) ? state.input : undefined;
|
|
141
|
+
const paths = input && OPENCODE_FILE_TOOLS.has(tool) ? absoluteToolPaths([input.filePath]) : undefined;
|
|
142
|
+
return {
|
|
143
|
+
stepId,
|
|
144
|
+
label: tool,
|
|
145
|
+
category: openCodeToolCategory(tool),
|
|
146
|
+
status: openCodeToolStatus(state),
|
|
147
|
+
body: clipToolSummary(openCodeToolBody(tool, state)),
|
|
148
|
+
...(input !== undefined ? { params: redactAndCapParams(input) } : {}),
|
|
149
|
+
...(paths ? { paths } : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Build the `tool` session event the chat/timeline consumes from a decoded step. `streaming` is
|
|
154
|
+
* true only while running; `stepId` drives the server's in-place running→terminal upsert. */
|
|
155
|
+
export function openCodeToolSessionEvent(step: OpenCodeToolStep, turnId: string | undefined): ProviderSessionEvent {
|
|
156
|
+
return {
|
|
157
|
+
type: "tool",
|
|
158
|
+
origin: "provider",
|
|
159
|
+
body: step.body,
|
|
160
|
+
label: step.label,
|
|
161
|
+
category: step.category,
|
|
162
|
+
status: step.status,
|
|
163
|
+
streaming: step.status === "running",
|
|
164
|
+
stepId: step.stepId,
|
|
165
|
+
...(turnId ? { turnId } : {}),
|
|
166
|
+
...(step.params !== undefined ? { params: step.params } : {}),
|
|
167
|
+
...(step.paths ? { paths: step.paths } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* #2413 — concatenate the terminal assistant message's text parts (wire order) into the turn-final
|
|
173
|
+
* response text. Non-text parts (tool, step-start/finish, reasoning) are skipped; the `\n\n` join
|
|
174
|
+
* matches the dashboard's block delimiter, so a multi-part response's leading blocks dedupe against
|
|
175
|
+
* the streamed narration. `undefined` when there is no assistant text.
|
|
176
|
+
*/
|
|
177
|
+
export function openCodeAssistantText(parts: unknown[] | undefined): string | undefined {
|
|
178
|
+
if (!parts) return undefined;
|
|
179
|
+
const blocks: string[] = [];
|
|
180
|
+
for (const part of parts) {
|
|
181
|
+
if (!isRecord(part) || stringValue(part.type) !== "text") continue;
|
|
182
|
+
const text = stringValue(part.text)?.trim();
|
|
183
|
+
if (text) blocks.push(text);
|
|
184
|
+
}
|
|
185
|
+
return blocks.length ? blocks.join("\n\n") : undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* #2605 — decode one `message.part.updated` reasoning part (`{ type: "reasoning", text }`,
|
|
190
|
+
* pinned v1.18.21, live-verified in the provider's own SQLite: opencode/minimax-m2.5-free AND
|
|
191
|
+
* kimi-for-coding sessions persist `{type:"reasoning",text}` parts) into the provider-neutral
|
|
192
|
+
* emit fields. Pure — the adapter owns the dedup/emission, exactly like decodeOpenCodeToolPart.
|
|
193
|
+
* Returns `undefined` for non-reasoning parts or empty text (an empty reasoning row is backend
|
|
194
|
+
* muzak, not operator signal — e.g. a session's first persisted reasoning part was `""`).
|
|
195
|
+
*/
|
|
196
|
+
export function decodeOpenCodeReasoningPart(part: unknown): { stepId: string; body: string } | undefined {
|
|
197
|
+
if (!isRecord(part)) return undefined;
|
|
198
|
+
if (stringValue(part.type) !== "reasoning") return undefined;
|
|
199
|
+
const stepId = stringValue(part.id);
|
|
200
|
+
const body = stringValue(part.text)?.trim();
|
|
201
|
+
if (!stepId || !body) return undefined;
|
|
202
|
+
return { stepId, body };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** #2605 — build the `reasoning` session event the chat/timeline consumes (TraceView reasoning
|
|
206
|
+
* row): `stepId` = the stable part id, so the server upserts the row in place on growth instead
|
|
207
|
+
* of appending a duplicate — the same stable-key shape the Codex adapter flushes with.
|
|
208
|
+
* `streaming` stays true while the part is unfinished (`time.end` unset), false on finalize. */
|
|
209
|
+
export function openCodeReasoningSessionEvent(step: { stepId: string; body: string }, turnId: string | undefined, streaming: boolean): ProviderSessionEvent {
|
|
210
|
+
return {
|
|
211
|
+
type: "reasoning",
|
|
212
|
+
origin: "provider",
|
|
213
|
+
body: step.body,
|
|
214
|
+
stepId: step.stepId,
|
|
215
|
+
streaming,
|
|
216
|
+
...(turnId ? { turnId } : {}),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* #2605 — one decoded reasoning part with its provider-side identity. The `id` is the wire that
|
|
222
|
+
* makes the turn-final backstop upsert instead of duplicate: the server keys step rows on
|
|
223
|
+
* `stepId` ONLY, so every turn-final reasoning row must carry the SAME stepId the live SSE door
|
|
224
|
+
* already emitted for that part (see openCodeReasoningSessionEvent) — a merged/concatenated
|
|
225
|
+
* body under a synthetic stepId would land as a second row on every turn whose SSE was
|
|
226
|
+
* observed. Thread `{id,text}[]` (not a joined string) through the completion for exactly this.
|
|
227
|
+
*/
|
|
228
|
+
export interface OpenCodeReasoningStep {
|
|
229
|
+
stepId: string;
|
|
230
|
+
body: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* #2605 — decode the completed assistant message's reasoning parts (wire order) into
|
|
235
|
+
* per-part steps. Mirrors openCodeAssistantText's part walk, but keeps identity: one entry per
|
|
236
|
+
* non-empty reasoning part (empty text is backend muzak — dropped by decodeOpenCodeReasoningPart
|
|
237
|
+
* — so it never occupies a row). `[]` when the message carried no reasoning.
|
|
238
|
+
*/
|
|
239
|
+
export function openCodeAssistantReasoningSteps(parts: unknown[] | undefined): OpenCodeReasoningStep[] {
|
|
240
|
+
if (!parts) return [];
|
|
241
|
+
const steps: OpenCodeReasoningStep[] = [];
|
|
242
|
+
for (const part of parts) {
|
|
243
|
+
const step = decodeOpenCodeReasoningPart(part);
|
|
244
|
+
if (step) steps.push(step);
|
|
245
|
+
}
|
|
246
|
+
return steps;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* #2605 — concatenate the terminal assistant message's reasoning parts (wire order) into the
|
|
251
|
+
* turn-final reasoning text. `undefined` when the completed message carried no (non-empty)
|
|
252
|
+
* reasoning. Kept for the completion log line / describeCompletion only — the EMIT path uses
|
|
253
|
+
* openCodeAssistantReasoningSteps (per-part identity) so live+final collapse to one row.
|
|
254
|
+
*/
|
|
255
|
+
export function openCodeAssistantReasoning(parts: unknown[] | undefined): string | undefined {
|
|
256
|
+
const steps = openCodeAssistantReasoningSteps(parts);
|
|
257
|
+
return steps.length ? steps.map((step) => step.body).join("\n\n") : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** #2413 — build the turn-final `response` event (marks it for Final Response capture / auto-forward
|
|
261
|
+
* and styles it apart from reasoning). `undefined` when the turn produced no assistant text. */
|
|
262
|
+
export function openCodeTurnFinalResponseEvent(text: string | undefined, turnId: string | undefined): ProviderSessionEvent | undefined {
|
|
263
|
+
const body = text?.trim();
|
|
264
|
+
if (!body) return undefined;
|
|
265
|
+
return { type: "response", origin: "provider", body, final: true, confidence: "definitive", endedBy: "completed", ...(turnId ? { turnId } : {}) };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* #2605 — emit the turn-final `reasoning` rows (`completion.reasoningSteps`, read from the SAME
|
|
270
|
+
* synchronous POST body as the response below) plus that response, in order. Each reasoning row
|
|
271
|
+
* carries its OWN part id as stepId — the exact key the live SSE door already emitted — so the
|
|
272
|
+
* server upserts live+final into ONE row instead of storing the turn's thinking twice. Pure
|
|
273
|
+
* event construction — the caller owns the ledger write + emit, like the tool/response builders.
|
|
274
|
+
*
|
|
275
|
+
* The parts are the model's thinking as of turn end — the completed message persists every
|
|
276
|
+
* reasoning part with its full text — so the backstop covers a POST-only reader, a dropped
|
|
277
|
+
* subscription, or frames racing the close. A part whose live row already carries this exact
|
|
278
|
+
* body is skipped here (same-body no-op, matching the SSE door's dedup); a part that GREW
|
|
279
|
+
* since its last live frame re-emits the current full body under the same stepId (upsert).
|
|
280
|
+
* The caller passes the per-part bodies it already emitted (or an empty record on the POST-only
|
|
281
|
+
* path) — comparison is scoped to THOSE part ids, never to a global body pool, so an identical
|
|
282
|
+
* body in a later turn is never misread as already-seen.
|
|
283
|
+
*
|
|
284
|
+
* The text is ground truth per #2389: the POST returning IS the provider-native turn-end edge,
|
|
285
|
+
* hence `confidence:"definitive"` on the response; `final:true` is what makes it a turn-final
|
|
286
|
+
* capture downstream. Emits reasoning first so the trace reads thinking-then-answer, the order
|
|
287
|
+
* the model produced them in. Either half may be absent (reasoning-only or text-only turns).
|
|
288
|
+
*/
|
|
289
|
+
export function openCodeTurnFinalEvents(
|
|
290
|
+
completion: { text?: string; reasoningSteps?: OpenCodeReasoningStep[] },
|
|
291
|
+
turnId: string | undefined,
|
|
292
|
+
emittedBodies: Record<string, string> = {},
|
|
293
|
+
): ProviderSessionEvent[] {
|
|
294
|
+
const events: ProviderSessionEvent[] = [];
|
|
295
|
+
for (const step of completion.reasoningSteps ?? []) {
|
|
296
|
+
if (emittedBodies[step.stepId] === step.body) continue;
|
|
297
|
+
events.push({ type: "reasoning", origin: "provider", body: step.body, stepId: step.stepId, streaming: false, ...(turnId ? { turnId } : {}) });
|
|
298
|
+
}
|
|
299
|
+
const response = openCodeTurnFinalResponseEvent(completion.text, turnId);
|
|
300
|
+
if (response) events.push(response);
|
|
301
|
+
return events;
|
|
302
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { harnessSubagentsEnabled } from "@humwick/sdk";
|
|
2
|
+
import type { RunnerSpawnConfig } from "../adapter";
|
|
3
|
+
import { openCodeNativeWebPermissions, openCodeWebToolsConfig } from "../web-tools";
|
|
4
|
+
|
|
5
|
+
export function openCodeHarnessTools(config: Pick<RunnerSpawnConfig, "harnessSubagents" | "agentProfile">): Record<string, false> {
|
|
6
|
+
return harnessSubagentsEnabled(config) ? {} : { task: false };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The opencode.json `tools` map: harness built-ins plus, with MCP present, the switched-off web tools. */
|
|
10
|
+
export function openCodeToolsConfig(config: Pick<RunnerSpawnConfig, "harnessSubagents" | "agentProfile">, hasMcp: boolean): { tools?: Record<string, false> } {
|
|
11
|
+
const tools = { ...openCodeHarnessTools(config), ...(hasMcp ? openCodeWebToolsConfig(config).tools : {}) };
|
|
12
|
+
return Object.keys(tools).length ? { tools } : {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function openCodePermissions(config: Pick<RunnerSpawnConfig, "harnessSubagents" | "agentProfile">): Record<string, "allow" | "deny"> {
|
|
16
|
+
return { "*": "allow", ...openCodeNativeWebPermissions(config) };
|
|
17
|
+
}
|