super-dsh 0.1.0 → 0.1.2
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/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- package/client.js +0 -4
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-claude provider — Claude Code provider plugin for DeepSeek Harness
|
|
3
|
+
* (Agent Worlds, standalone SDK line).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the built-in agent loop with an `AgentFactory` that drives one
|
|
6
|
+
* `ClaudeSdkClient` (a per-session Claude Agent SDK bridge) and bridges the
|
|
7
|
+
* projected wire stream into the Dash Agent/Session contracts. Mirrors
|
|
8
|
+
* `@deepseek-ai/dsh-agent-loop`'s creation transaction (prepare → owned
|
|
9
|
+
* storage handle → setup → publish) so the session + agent publish as one
|
|
10
|
+
* ordered lifecycle AND the DSH session log is written through the upstream
|
|
11
|
+
* `sessionPersistence` service — list/read/replay/workspace grouping are then
|
|
12
|
+
* ordinary DSH services over that log.
|
|
13
|
+
*
|
|
14
|
+
* Session identity (route A, "mapping only"): the Claude session id is derived
|
|
15
|
+
* deterministically from the DSH id's UUID tail (`claudeSessionIdFromDsh`), so
|
|
16
|
+
* the id itself survives restarts with no map. The effective Claude permission
|
|
17
|
+
* mode cannot round-trip through the event log (extra tiers are skipped by
|
|
18
|
+
* `presetFromEvents`), so it is persisted at the world DSH-home root alongside
|
|
19
|
+
* the `claudeSessionId` (`<worldHome>/dsh-sessions.json`, see session-map.ts)
|
|
20
|
+
* and re-applied on resume via `client.setPermissionMode(mode)`.
|
|
21
|
+
*/
|
|
22
|
+
import { realpathSync, statSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
25
|
+
import { emitAgentEvent } from "@deepseek-ai/dsh-agent";
|
|
26
|
+
import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
|
|
27
|
+
import { ClaudeSdkClient, probeClaudeModels } from "./claude-client.js";
|
|
28
|
+
import { ClaudeAgent } from "./agent.js";
|
|
29
|
+
import { ClaudeLlmAdapter, CLAUDE_PROVIDER_ID } from "./adapter.js";
|
|
30
|
+
import { resolveClaudeHome } from "./claude-home.js";
|
|
31
|
+
import { resetProjectionState } from "./claude-events.js";
|
|
32
|
+
import { claudeSessionIdFromDsh } from "./session-id.js";
|
|
33
|
+
import { claudePermissionMode, defaultPermissionPreset, modeChangePatch, presetFromEvents, } from "./permission.js";
|
|
34
|
+
import { sessionRecord, upsertSession } from "./session-map.js";
|
|
35
|
+
import { makeCanUseTool, answerAskUserQuestion, askPlanReview, } from "./interaction.js";
|
|
36
|
+
import { modelEntryFromSdk, readModelCatalog, setModelCatalog, CLAUDE_DEFAULT_MODEL } from "./models.js";
|
|
37
|
+
import { SingleClaudePresetRoster } from "./agent-preset-claude.js";
|
|
38
|
+
import { claudeAgentPresetProjection } from "./agent-preset-projection.js";
|
|
39
|
+
const CLAUDE_MODES = ["default", "acceptEdits", "bypassPermissions", "plan", "auto", "dontAsk"];
|
|
40
|
+
/** Narrow an untrusted value to a persisted Claude permission mode. */
|
|
41
|
+
function isClaudeMode(raw) {
|
|
42
|
+
return typeof raw === "string" && CLAUDE_MODES.includes(raw);
|
|
43
|
+
}
|
|
44
|
+
/** Realpath of a recorded cwd, accepted only when it names an existing directory. */
|
|
45
|
+
function validatedCwd(cwd) {
|
|
46
|
+
if (cwd === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
try {
|
|
49
|
+
const canonical = realpathSync(cwd);
|
|
50
|
+
return statSync(canonical).isDirectory() ? canonical : undefined;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Diagnostic trace (CLAUDE_TRACE=1 on the dsh process enables stderr tracing). */
|
|
57
|
+
const trace = (...parts) => {
|
|
58
|
+
if (process.env.CLAUDE_TRACE === "1")
|
|
59
|
+
process.stderr.write(`[claude-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Resume identity guard (exported for unit testing): fail closed when no Claude
|
|
63
|
+
* session is recorded for the DSH session id. Route B (a mapping file) is V1+
|
|
64
|
+
* work, so a missing/empty id is a genuine unknown, never a silent new session.
|
|
65
|
+
*/
|
|
66
|
+
export function resumeGuard(record) {
|
|
67
|
+
if (typeof record.claudeSessionId !== "string" || record.claudeSessionId === "") {
|
|
68
|
+
throw new Error(`cannot resume session "${record.dshSessionId}": no Claude session is recorded for this Dash session id`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The host-side `canUseTool` dispatcher the provider injects into the SDK
|
|
73
|
+
* options. Claude Code routes every human permission decision through exactly
|
|
74
|
+
* three channels — the `AskUserQuestion` tool, `ExitPlanMode` (plan review),
|
|
75
|
+
* and every other tool's permission gate — and this dispatcher fans each onto
|
|
76
|
+
* the DSH native seams. A missing seam fails CLOSED (deny), never silently
|
|
77
|
+
* lets the CLI decide. Approvals are wrapped in the agent's `runApproval` so
|
|
78
|
+
* the idle-TTL gate protects a session waiting on a human.
|
|
79
|
+
*/
|
|
80
|
+
export function makeClaudeCanUseTool(deps) {
|
|
81
|
+
return async (toolName, input, options) => {
|
|
82
|
+
const agent = deps.getAgent();
|
|
83
|
+
const approval = deps.approval;
|
|
84
|
+
const questions = deps.questions;
|
|
85
|
+
const run = (task) => (agent === undefined ? task() : agent.runApproval(task));
|
|
86
|
+
const deny = (message) => ({ behavior: "deny", message });
|
|
87
|
+
if (toolName === "AskUserQuestion") {
|
|
88
|
+
if (questions === undefined)
|
|
89
|
+
return deny("Claude Code AskUserQuestion denied (no user-questions seam)");
|
|
90
|
+
return run(() => answerAskUserQuestion({ questions, agent }, input, options.signal));
|
|
91
|
+
}
|
|
92
|
+
if (toolName === "ExitPlanMode") {
|
|
93
|
+
if (questions === undefined)
|
|
94
|
+
return deny("Claude Code ExitPlanMode denied (no user-questions seam)");
|
|
95
|
+
const plan = input !== null && typeof input === "object"
|
|
96
|
+
? input.plan
|
|
97
|
+
: undefined;
|
|
98
|
+
if (typeof plan !== "string" || plan.trim() === "") {
|
|
99
|
+
// Fail CLOSED on malformed input: dereferencing a null plan here used
|
|
100
|
+
// to reject, which the SDK reads as a denial but reports as noise.
|
|
101
|
+
return deny("Claude Code ExitPlanMode denied (malformed input: no plan)");
|
|
102
|
+
}
|
|
103
|
+
const approved = await run(async () => askPlanReview({ questions, agent }, plan, options.signal));
|
|
104
|
+
return approved ? { behavior: "allow" } : deny("Claude Code ExitPlanMode denied (plan not approved)");
|
|
105
|
+
}
|
|
106
|
+
if (approval === undefined)
|
|
107
|
+
return deny("Claude Code tool call denied (no approval seam)");
|
|
108
|
+
return run(() => makeCanUseTool({ approval, agent })(toolName, input, {
|
|
109
|
+
toolUseID: options.toolUseID,
|
|
110
|
+
signal: options.signal,
|
|
111
|
+
...(options.decisionReason === undefined ? {} : { decisionReason: options.decisionReason }),
|
|
112
|
+
}));
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export class ClaudeProvider extends Service {
|
|
116
|
+
static inject = ["agents", "sessions", "llm", "agentDefaultModel", "settings"];
|
|
117
|
+
/** Plain holder — prevents Cordis re-tracing the factory's ctx through a caller shadow. */
|
|
118
|
+
runtime;
|
|
119
|
+
/** The world's DSH home — from the world's own `dshHomePath`, never ambient env inside plugins. Anchors adapter-owned DSH state (the session map) only; the Claude app home is the CLI's native `~/.claude` (2026-09-17 ruling). */
|
|
120
|
+
home;
|
|
121
|
+
/** Echo/staleness guard for the app-wide default-model push. */
|
|
122
|
+
defaultModelKey = "";
|
|
123
|
+
constructor(ctx) {
|
|
124
|
+
super(ctx, "claudeProvider");
|
|
125
|
+
this.runtime = { ctx };
|
|
126
|
+
this.home = this.#resolveHome();
|
|
127
|
+
// Native-home ruling (2026-09-17): no CLAUDE_CONFIG_DIR injection — the CLI
|
|
128
|
+
// keeps its native home; this.home only anchors the DSH-side session map.
|
|
129
|
+
trace(`home: worldHome=${this.home} claudeHome=${resolveClaudeHome()}`);
|
|
130
|
+
ctx.effect(() => ctx.agents.setFactory(this), "claudeProvider.setFactory()");
|
|
131
|
+
this.#registerModelCatalog();
|
|
132
|
+
// The roster must be visible to the API gateway's root-level remote
|
|
133
|
+
// enumeration; nesting it under a child fiber via ctx.plugin hides its
|
|
134
|
+
// @Remote routes from the gateway and every agentPresets/* call 404s (the
|
|
135
|
+
// omp/pi registration lesson). Register it on this plugin's own
|
|
136
|
+
// (top-level) fiber instead — next to setFactory, never under ctx.plugin.
|
|
137
|
+
new SingleClaudePresetRoster(ctx);
|
|
138
|
+
// Drive the `agentPreset` session projection ourselves: the upstream
|
|
139
|
+
// registrant lives in the (patch-disabled) dsh-agent-presets package, but
|
|
140
|
+
// the Web UI gates the preset chip and header label on
|
|
141
|
+
// `projectionValues.agentPreset`.
|
|
142
|
+
ctx.inject(["sessionProjections"], (scoped) => {
|
|
143
|
+
scoped.sessionProjections.register(claudeAgentPresetProjection);
|
|
144
|
+
});
|
|
145
|
+
this.#registerDefaultModel();
|
|
146
|
+
this.#probeBootModels();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* RC-3 boot model probe: ONE ephemeral SDK query asks the CLI's
|
|
150
|
+
* `supportedModels()` right after the adapter registration, and the answer
|
|
151
|
+
* OVERWRITES the sonnet-only boot catalog (setModelCatalog semantics).
|
|
152
|
+
* Fire-and-forget: a slow/failing probe never delays boot and never
|
|
153
|
+
* touches the boot catalog — `sonnet` (a real CLI alias) stays the
|
|
154
|
+
* probe-failure fallback, and a live session re-observes anyway.
|
|
155
|
+
*/
|
|
156
|
+
#probeBootModels() {
|
|
157
|
+
void probeClaudeModels()
|
|
158
|
+
.then((models) => {
|
|
159
|
+
const entries = models.flatMap((model) => modelEntryFromSdk(model));
|
|
160
|
+
if (entries.length === 0) {
|
|
161
|
+
trace("boot model probe: the CLI reported no models — boot catalog (sonnet) kept");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
setModelCatalog(entries, CLAUDE_DEFAULT_MODEL);
|
|
165
|
+
this.runtime.ctx.logger.info(`claude-provider: boot model probe ← ${entries.length} CLI model(s), default ${CLAUDE_DEFAULT_MODEL}`);
|
|
166
|
+
})
|
|
167
|
+
.catch((error) => {
|
|
168
|
+
trace(`boot model probe failed (${String(error)}) — boot catalog (sonnet) kept`);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/** The world's DSH home: boot-provided dshHomePath first, env last. */
|
|
172
|
+
#resolveHome() {
|
|
173
|
+
const fromBoot = this.runtime.ctx.get("dshHomePath");
|
|
174
|
+
if (typeof fromBoot === "string" && fromBoot !== "")
|
|
175
|
+
return fromBoot;
|
|
176
|
+
if (typeof fromBoot === "function") {
|
|
177
|
+
const resolved = fromBoot();
|
|
178
|
+
if (typeof resolved === "string" && resolved !== "")
|
|
179
|
+
return resolved;
|
|
180
|
+
}
|
|
181
|
+
return process.env.DSH_HOME ?? join(process.cwd(), ".tests", "aw");
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* The write path is mandatory (2026-09-16): this factory owns the DSH
|
|
185
|
+
* session log through the upstream persistence service — a session created
|
|
186
|
+
* or resumed without a backend would persist nothing.
|
|
187
|
+
*/
|
|
188
|
+
requirePersistence(op) {
|
|
189
|
+
const persistence = this.runtime.ctx.get("sessionPersistence");
|
|
190
|
+
if (persistence === undefined) {
|
|
191
|
+
throw new Error(`cannot ${op} a Claude session: session persistence is not configured`);
|
|
192
|
+
}
|
|
193
|
+
return persistence;
|
|
194
|
+
}
|
|
195
|
+
async createStoredSession(persistence, session, signal) {
|
|
196
|
+
const handle = await persistence.create(session.header, {
|
|
197
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
198
|
+
...(signal === undefined ? {} : { signal }),
|
|
199
|
+
});
|
|
200
|
+
return { handle, storedCount: 0 };
|
|
201
|
+
}
|
|
202
|
+
async createAgent(ownerCtx, options) {
|
|
203
|
+
const loopCtx = this.runtime.ctx;
|
|
204
|
+
const id = options.sessionId;
|
|
205
|
+
const meta = options.meta ?? {};
|
|
206
|
+
const cwd = meta.cwd;
|
|
207
|
+
// Fork is rejected in V1 (the SDK has `forkSession`, but it is not wired).
|
|
208
|
+
if (meta.parentSession !== undefined) {
|
|
209
|
+
throw new Error(`cannot fork session "${meta.parentSession}": fork is not wired in V1`);
|
|
210
|
+
}
|
|
211
|
+
// Route A: derive the Claude session id before anything is spawned.
|
|
212
|
+
const claudeSessionId = claudeSessionIdFromDsh(String(id));
|
|
213
|
+
if (claudeSessionId === undefined) {
|
|
214
|
+
trace(`create ${id}: DSH id carries no UUID and no mapping exists — route B is not wired in V1`);
|
|
215
|
+
throw new Error(`cannot create session "${id}": the Dash session id carries no UUID and route B is not wired in V1`);
|
|
216
|
+
}
|
|
217
|
+
const persistence = this.requirePersistence("create");
|
|
218
|
+
// Single-source permission resolution: one preset → one Claude mode, which
|
|
219
|
+
// is persisted in the session record AND handed to the agent (so the
|
|
220
|
+
// `permission/preset` stamp and the `setPermissionMode` call never diverge).
|
|
221
|
+
const preset = defaultPermissionPreset(loopCtx);
|
|
222
|
+
const claudeMode = claudePermissionMode(preset);
|
|
223
|
+
const spawnCwd = validatedCwd(cwd) ?? process.cwd();
|
|
224
|
+
trace(`create id=${id} claudeSessionId=${claudeSessionId} preset=${preset ?? "none"} claudeMode=${claudeMode} spawnCwd=${spawnCwd}`);
|
|
225
|
+
// Lazy, zero-IO client construction (the CLI spawns on the first prompt).
|
|
226
|
+
// The host-side permission gate is injected now, closing over the injected
|
|
227
|
+
// approval/questions seams plus a lazy agent holder (set in setupAndPublish).
|
|
228
|
+
const agentRef = { current: undefined };
|
|
229
|
+
const client = new ClaudeSdkClient({
|
|
230
|
+
cwd: spawnCwd,
|
|
231
|
+
claudeSessionId,
|
|
232
|
+
canUseTool: makeClaudeCanUseTool({
|
|
233
|
+
approval: loopCtx.get("approval"),
|
|
234
|
+
questions: loopCtx.get("userQuestions"),
|
|
235
|
+
getAgent: () => agentRef.current,
|
|
236
|
+
}),
|
|
237
|
+
allowDangerouslySkipPermissions: claudeMode === "bypassPermissions",
|
|
238
|
+
});
|
|
239
|
+
// Apply the resolved boot/observed default to the CLI (mirrors codex's
|
|
240
|
+
// create-time `setModel`); an explicit caller selection is honored by
|
|
241
|
+
// `sessionAgentOptions` at publication.
|
|
242
|
+
const createCatalog = readModelCatalog();
|
|
243
|
+
void client.setModel(createCatalog.defaultModel ?? CLAUDE_DEFAULT_MODEL).catch(() => { });
|
|
244
|
+
const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
245
|
+
...(options.seed === undefined ? {} : { seed: options.seed }),
|
|
246
|
+
...(meta === undefined ? {} : { meta }),
|
|
247
|
+
...(options.inheritedEventCount === undefined ? {} : { inheritedEventCount: options.inheritedEventCount }),
|
|
248
|
+
}));
|
|
249
|
+
let stored;
|
|
250
|
+
let handedOff = false;
|
|
251
|
+
try {
|
|
252
|
+
stored = await this.createStoredSession(persistence, preparation.session, options.signal);
|
|
253
|
+
// Identity + permission record BEFORE publication: resume resolves it.
|
|
254
|
+
upsertSession(this.home, String(id), {
|
|
255
|
+
claudeSessionId,
|
|
256
|
+
claudeMode,
|
|
257
|
+
cwd: spawnCwd,
|
|
258
|
+
createdAt: Date.now(),
|
|
259
|
+
preset: preset ?? null,
|
|
260
|
+
});
|
|
261
|
+
handedOff = true;
|
|
262
|
+
return await setupAndPublish(loopCtx, ownerCtx, id, this.sessionAgentOptions(options.agentOptions), options.setup, preparation.session, client, options.parentAgent, "startup", { enterSession: true, preparation }, stored, {
|
|
263
|
+
preset,
|
|
264
|
+
claudeMode,
|
|
265
|
+
onModeChange: (mode) => {
|
|
266
|
+
// Upsert {claudeMode, preset} TOGETHER (RC-5): a skeleton mode
|
|
267
|
+
// names its preset via the inverse table; an extra tier
|
|
268
|
+
// (plan/auto/dontAsk) leaves the recorded preset alone.
|
|
269
|
+
upsertSession(this.home, String(id), modeChangePatch(mode));
|
|
270
|
+
trace(`persisted claudeMode ${mode} for ${id}`);
|
|
271
|
+
},
|
|
272
|
+
onNotResumable: () => {
|
|
273
|
+
// 2026-09-18 ruling: a conversation-not-found failure is terminal
|
|
274
|
+
// for the pairing — mark the record dead; later resumes fail fast.
|
|
275
|
+
upsertSession(this.home, String(id), { resumable: false });
|
|
276
|
+
trace(`marked ${id} not resumable (conversation never materialized)`);
|
|
277
|
+
},
|
|
278
|
+
}, agentRef);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
trace(`create ${id} THREW: ${String(error)}`);
|
|
282
|
+
client.close();
|
|
283
|
+
if (!handedOff)
|
|
284
|
+
await stored?.handle.close().catch(() => { });
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
if (!handedOff)
|
|
289
|
+
preparation[Symbol.dispose]();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async resume(ownerCtx, options) {
|
|
293
|
+
const loopCtx = this.runtime.ctx;
|
|
294
|
+
const id = options.resumeSessionId;
|
|
295
|
+
// Identity (2026-09-16 "mapping only"): the DSH session id resolves
|
|
296
|
+
// through the adapter-owned record. A missing record (or a missing/empty
|
|
297
|
+
// Claude session id) is a genuine unknown: fail closed with the DSH id.
|
|
298
|
+
const record = sessionRecord(this.home, String(id));
|
|
299
|
+
trace(`resume id=${id} record=${record === undefined ? "MISSING" : record.claudeSessionId}`);
|
|
300
|
+
resumeGuard({ dshSessionId: String(id), claudeSessionId: record === undefined ? undefined : record.claudeSessionId });
|
|
301
|
+
if (record === undefined) {
|
|
302
|
+
// Unreachable (resumeGuard threw above) — narrows `record` for TS.
|
|
303
|
+
throw new Error(`cannot resume session "${id}": no Claude session is recorded for this Dash session id`);
|
|
304
|
+
}
|
|
305
|
+
if (record.resumable === false) {
|
|
306
|
+
// 2026-09-18 ruling: the CLI never materialized a conversation under the
|
|
307
|
+
// derived id (e.g. the first turn failed). The session is dead — fail
|
|
308
|
+
// fast with the stable code; never resurrect, never re-wrap content.
|
|
309
|
+
const error = new Error(`cannot resume session "${id}": its Claude conversation never materialized (the first exchange failed before the CLI persisted one) — start a new session`);
|
|
310
|
+
error.code = "CONVERSATION_NOT_FOUND";
|
|
311
|
+
throw error;
|
|
312
|
+
}
|
|
313
|
+
const claudeSessionId = record.claudeSessionId;
|
|
314
|
+
// Spawn cwd comes from the recorded pairing — re-realpath and re-verify so
|
|
315
|
+
// a moved/removed directory cannot aim a resume outside a live path.
|
|
316
|
+
const spawnCwd = validatedCwd(record.cwd);
|
|
317
|
+
if (spawnCwd === undefined) {
|
|
318
|
+
throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
|
|
319
|
+
}
|
|
320
|
+
const persistence = this.requirePersistence("resume");
|
|
321
|
+
// Permission re-mount (RC-5): the LOG FOLD is the preset authority —
|
|
322
|
+
// resolved from the cold events once they are read below. The map's
|
|
323
|
+
// `claudeMode` remains ONLY the mode cache for `setPermissionMode` (extra
|
|
324
|
+
// tiers cannot round-trip through the log); a missing/unknown cache falls
|
|
325
|
+
// back to the default preset's mode until the fold is read — traced,
|
|
326
|
+
// never silent.
|
|
327
|
+
let claudeMode;
|
|
328
|
+
if (isClaudeMode(record.claudeMode)) {
|
|
329
|
+
claudeMode = record.claudeMode;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
claudeMode = claudePermissionMode(defaultPermissionPreset(loopCtx));
|
|
333
|
+
trace(`resume ${id}: no usable claudeMode recorded (${String(record.claudeMode)}) — falling back to ${claudeMode} until the log fold is read`);
|
|
334
|
+
}
|
|
335
|
+
trace(`resume id=${id} spawnCwd=${spawnCwd} claudeMode=${claudeMode} (preset resolved from the log fold below)`);
|
|
336
|
+
// Re-attach to the Claude session by its id (lazy: the CLI resumes on the
|
|
337
|
+
// first prompt). Re-apply the persisted permission mode up front (buffered
|
|
338
|
+
// until the query exists), and re-inject the host-side permission gate.
|
|
339
|
+
const agentRef = { current: undefined };
|
|
340
|
+
const client = new ClaudeSdkClient({
|
|
341
|
+
cwd: spawnCwd,
|
|
342
|
+
resumeSessionId: claudeSessionId,
|
|
343
|
+
canUseTool: makeClaudeCanUseTool({
|
|
344
|
+
approval: loopCtx.get("approval"),
|
|
345
|
+
questions: loopCtx.get("userQuestions"),
|
|
346
|
+
getAgent: () => agentRef.current,
|
|
347
|
+
}),
|
|
348
|
+
allowDangerouslySkipPermissions: claudeMode === "bypassPermissions",
|
|
349
|
+
});
|
|
350
|
+
void client.setPermissionMode(claudeMode).catch(() => { });
|
|
351
|
+
let handle;
|
|
352
|
+
let stored;
|
|
353
|
+
let preparation;
|
|
354
|
+
let handedOff = false;
|
|
355
|
+
try {
|
|
356
|
+
// Taking write ownership FIRST excludes a concurrent resume of the same id.
|
|
357
|
+
handle = await persistence.open(id, "write", options.signal === undefined ? {} : { signal: options.signal });
|
|
358
|
+
const cold = await handle.read(0, undefined, options.signal === undefined ? {} : { signal: options.signal });
|
|
359
|
+
const closers = interruptedTurnClosers(cold.events);
|
|
360
|
+
if (closers.length > 0)
|
|
361
|
+
await handle.append(closers);
|
|
362
|
+
preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
363
|
+
seed: [...cold.events, ...closers],
|
|
364
|
+
meta: structuredClone(handle.header),
|
|
365
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
366
|
+
eventState: cold.eventState,
|
|
367
|
+
}));
|
|
368
|
+
stored = { handle, storedCount: cold.events.length + closers.length };
|
|
369
|
+
trace(`resume id=${id} seeded ${cold.events.length} events (+${closers.length} closers)`);
|
|
370
|
+
handle = undefined; // ownership passes to setupAndPublish
|
|
371
|
+
handedOff = true;
|
|
372
|
+
// RC-5: the log fold decides the preset at resume — the map's stale
|
|
373
|
+
// `preset` must never overwrite a blank-window choice already folded
|
|
374
|
+
// into the log. The mode cache stays authoritative for the wire unless
|
|
375
|
+
// it was unusable, in which case the folded preset derives the mode and
|
|
376
|
+
// it is re-applied now that the fold is read.
|
|
377
|
+
const foldedPreset = presetFromEvents(cold.events) ?? defaultPermissionPreset(loopCtx);
|
|
378
|
+
if (!isClaudeMode(record.claudeMode)) {
|
|
379
|
+
claudeMode = claudePermissionMode(foldedPreset);
|
|
380
|
+
void client.setPermissionMode(claudeMode).catch(() => { });
|
|
381
|
+
trace(`resume ${id}: mode cache unusable — log fold preset=${foldedPreset ?? "none"} → claudeMode ${claudeMode} (re-applied)`);
|
|
382
|
+
}
|
|
383
|
+
trace(`resume id=${id} fold preset=${foldedPreset ?? "none"} claudeMode=${claudeMode}`);
|
|
384
|
+
return await setupAndPublish(loopCtx, ownerCtx, id, this.sessionAgentOptions(options.agentOptions), options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, {
|
|
385
|
+
// RC-5: the log fold decides the preset at resume — the map's stale
|
|
386
|
+
// `preset` must never overwrite a blank-window choice already folded
|
|
387
|
+
// into the log. The mode cache stays authoritative for the wire
|
|
388
|
+
// unless it was unusable, in which case the folded preset derives it.
|
|
389
|
+
preset: foldedPreset,
|
|
390
|
+
claudeMode,
|
|
391
|
+
onModeChange: (mode) => {
|
|
392
|
+
// Upsert {claudeMode, preset} TOGETHER (RC-5): a skeleton mode
|
|
393
|
+
// names its preset via the inverse table; an extra tier
|
|
394
|
+
// (plan/auto/dontAsk) leaves the recorded preset alone.
|
|
395
|
+
upsertSession(this.home, String(id), modeChangePatch(mode));
|
|
396
|
+
trace(`persisted claudeMode ${mode} for ${id}`);
|
|
397
|
+
},
|
|
398
|
+
onNotResumable: () => {
|
|
399
|
+
// 2026-09-18 ruling: a conversation-not-found failure is terminal
|
|
400
|
+
// for the pairing — mark the record dead; later resumes fail fast.
|
|
401
|
+
upsertSession(this.home, String(id), { resumable: false });
|
|
402
|
+
trace(`marked ${id} not resumable (conversation never materialized)`);
|
|
403
|
+
},
|
|
404
|
+
}, agentRef);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
|
|
408
|
+
client.close();
|
|
409
|
+
if (!handedOff)
|
|
410
|
+
await handle?.close().catch(() => { });
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
if (!handedOff)
|
|
415
|
+
preparation?.[Symbol.dispose]();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
#registerModelCatalog() {
|
|
419
|
+
const llm = this.runtime.ctx.get("llm");
|
|
420
|
+
if (llm === undefined)
|
|
421
|
+
return;
|
|
422
|
+
llm.registerAdapter([CLAUDE_PROVIDER_ID], new ClaudeLlmAdapter());
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The factory owns its sessions' route (user directive 2026-09-15): the
|
|
426
|
+
* ambient `agentDefaultModel` selection is NOT authoritative here — a session
|
|
427
|
+
* created by this factory always runs on the `claude` route, with the boot
|
|
428
|
+
* default (or an observed default) unless the caller asked for a served
|
|
429
|
+
* claude model.
|
|
430
|
+
*/
|
|
431
|
+
sessionAgentOptions(requested) {
|
|
432
|
+
const catalog = readModelCatalog();
|
|
433
|
+
const explicit = requested?.provider === CLAUDE_PROVIDER_ID && typeof requested.model === "string" && requested.model !== ""
|
|
434
|
+
? requested.model
|
|
435
|
+
: undefined;
|
|
436
|
+
const model = explicit ?? catalog.defaultModel ?? CLAUDE_DEFAULT_MODEL;
|
|
437
|
+
return { ...(requested ?? {}), provider: CLAUDE_PROVIDER_ID, ...(model === undefined ? {} : { model }) };
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* App-wide default-model push (mirrors `agent-codex` `#registerDefaultModel`
|
|
441
|
+
* faithfully): the harness owns the "default for new sessions" selection in
|
|
442
|
+
* `agentDefaultModel` (mounted by dsh-base with a hard-coded
|
|
443
|
+
* `deepseek-official/deepseek-flash`), and this bridge is the only thing that
|
|
444
|
+
* feeds it a claude-routed default. Without the inject the context cannot
|
|
445
|
+
* resolve the service at apply time and the push never fires. The write goes
|
|
446
|
+
* through `settings.replace` AND `saveSelection` (the double path that avoids
|
|
447
|
+
* the upstream silent no-op) with an echo-guarded `defaultModelKey`.
|
|
448
|
+
*/
|
|
449
|
+
#registerDefaultModel() {
|
|
450
|
+
this.runtime.ctx.inject(["agentDefaultModel", "settings"], (mCtx) => {
|
|
451
|
+
const service = mCtx.get("agentDefaultModel");
|
|
452
|
+
if (service === undefined)
|
|
453
|
+
return;
|
|
454
|
+
const catalog = readModelCatalog();
|
|
455
|
+
const target = { provider: CLAUDE_PROVIDER_ID, model: catalog.defaultModel ?? CLAUDE_DEFAULT_MODEL };
|
|
456
|
+
const targetKey = `${target.provider}/${target.model}`;
|
|
457
|
+
const current = service.currentSelection?.();
|
|
458
|
+
const currentKey = current === undefined ? undefined : `${current.provider}/${current.model}`;
|
|
459
|
+
if (currentKey === targetKey || currentKey === this.defaultModelKey)
|
|
460
|
+
return;
|
|
461
|
+
trace(`default model: ${currentKey ?? "none"} → ${targetKey}`);
|
|
462
|
+
const settings = mCtx.get("settings");
|
|
463
|
+
void Promise.resolve(settings?.replace?.("agent-default-model", target))
|
|
464
|
+
.then(() => service.saveSelection?.(target))
|
|
465
|
+
.then(() => {
|
|
466
|
+
this.defaultModelKey = targetKey;
|
|
467
|
+
this.runtime.ctx.logger.info(`claude-provider: default model ← claude catalog: ${targetKey}`);
|
|
468
|
+
})
|
|
469
|
+
.catch((error) => {
|
|
470
|
+
this.runtime.ctx.logger.warn(`claude-provider: default model save failed: ${String(error)}`);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Durable flush of the session's pre-publication suffix. Constructor seed
|
|
477
|
+
* markers and setup-window events never re-emit through `session/event`, so
|
|
478
|
+
* publication must push them through the handle before live events start
|
|
479
|
+
* routing into it.
|
|
480
|
+
*/
|
|
481
|
+
async function appendUnstoredSuffix(stored, session) {
|
|
482
|
+
const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
|
|
483
|
+
if (suffix.length > 0)
|
|
484
|
+
await stored.handle.append(suffix);
|
|
485
|
+
stored.storedCount += suffix.length;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Shared creation/resume transaction: build the agent over the prepared
|
|
489
|
+
* session, run unpublished setup, flush the unstored suffix, and publish both
|
|
490
|
+
* in order; the owned storage handle is drained (closed) on teardown. Kept as
|
|
491
|
+
* a module-level function (not a private method) because Cordis exposes the
|
|
492
|
+
* provider through a tracing proxy, which breaks hard-private receivers.
|
|
493
|
+
*/
|
|
494
|
+
async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo, agentRef) {
|
|
495
|
+
let detachSession;
|
|
496
|
+
let detachAgent;
|
|
497
|
+
let agent;
|
|
498
|
+
let idleExit;
|
|
499
|
+
try {
|
|
500
|
+
// Session-start projection reset lives in the agent constructor; this
|
|
501
|
+
// module-level reset is belt-and-suspenders for a re-entry before the
|
|
502
|
+
// agent exists (e.g. a throwing setup that re-publishes).
|
|
503
|
+
resetProjectionState();
|
|
504
|
+
agent = new ClaudeAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo);
|
|
505
|
+
agentRef.current = agent;
|
|
506
|
+
// Composition-only setup on the unpublished agent scope.
|
|
507
|
+
const commit = await setup?.(agent.ctx, agent);
|
|
508
|
+
// Publish: flush the unstored suffix, enter + announce in order.
|
|
509
|
+
commit?.commit();
|
|
510
|
+
await appendUnstoredSuffix(stored, session);
|
|
511
|
+
if (opts.enterSession) {
|
|
512
|
+
detachSession = agent.ctx.sessions.enter(session);
|
|
513
|
+
agent.ctx.sessions.announce(session);
|
|
514
|
+
}
|
|
515
|
+
detachAgent = loopCtx.agents.enter(agent, parentAgent);
|
|
516
|
+
loopCtx.agents.announce(agent);
|
|
517
|
+
emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
|
|
518
|
+
let disposed = false;
|
|
519
|
+
let unfollowOwner;
|
|
520
|
+
const dispose = async () => {
|
|
521
|
+
if (disposed)
|
|
522
|
+
return;
|
|
523
|
+
disposed = true;
|
|
524
|
+
trace(`dispose() called for agent ${id} (source=${source})`);
|
|
525
|
+
unfollowOwner?.();
|
|
526
|
+
await agent?.dispose();
|
|
527
|
+
detachAgent?.();
|
|
528
|
+
detachSession?.();
|
|
529
|
+
await stored.handle.close();
|
|
530
|
+
};
|
|
531
|
+
idleExit = () => {
|
|
532
|
+
void dispose().catch((error) => {
|
|
533
|
+
loopCtx.logger.warn(`claude-provider: teardown of ${id} failed: ${String(error)}`);
|
|
534
|
+
});
|
|
535
|
+
};
|
|
536
|
+
unfollowOwner = ownerCtx.effect(() => () => {
|
|
537
|
+
void dispose().catch((error) => {
|
|
538
|
+
loopCtx.logger.warn(`claude-provider: owner-unload teardown of ${id} failed: ${String(error)}`);
|
|
539
|
+
});
|
|
540
|
+
}, `claudeProvider.lifecycle(${id})`);
|
|
541
|
+
return { agent, dispose };
|
|
542
|
+
}
|
|
543
|
+
catch (error) {
|
|
544
|
+
trace(`setupAndPublish ${id} FAILED: ${String(error)}`);
|
|
545
|
+
detachAgent?.();
|
|
546
|
+
detachSession?.();
|
|
547
|
+
void agent?.dispose().catch(() => { });
|
|
548
|
+
client.close();
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
opts.preparation?.[Symbol.dispose]();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
export default ClaudeProvider;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge the widened content union to the SDK's MessageParam content.
|
|
3
|
+
*
|
|
4
|
+
* A plain string keeps the historical normalization into ONE text block (the
|
|
5
|
+
* SDK's own transport shape, and what the client's shape pin asserts); a block
|
|
6
|
+
* array is handed through AS-IS, so the transcoder's one-block-per-input-block
|
|
7
|
+
* shape is never collapsed. The transcoder emits Anthropic text/image blocks;
|
|
8
|
+
* the SDK narrows an image source's `media_type` to its four raster literals,
|
|
9
|
+
* so one boundary assertion spans the two spellings (the transcoder gates the
|
|
10
|
+
* media type).
|
|
11
|
+
*/
|
|
12
|
+
function sdkMessageContent(content) {
|
|
13
|
+
if (typeof content === "string")
|
|
14
|
+
return [{ type: "text", text: content }];
|
|
15
|
+
return content;
|
|
16
|
+
}
|
|
17
|
+
export class InputQueue {
|
|
18
|
+
pending = [];
|
|
19
|
+
waiter;
|
|
20
|
+
done = false;
|
|
21
|
+
get closed() {
|
|
22
|
+
return this.done;
|
|
23
|
+
}
|
|
24
|
+
push(message) {
|
|
25
|
+
if (this.done)
|
|
26
|
+
throw new Error("agent-claude: input queue is closed");
|
|
27
|
+
this.pending.push(message);
|
|
28
|
+
const waiter = this.waiter;
|
|
29
|
+
this.waiter = undefined;
|
|
30
|
+
waiter?.();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Push one user turn whose `content` is the widened prompt surface (plain
|
|
34
|
+
* text or transcoded Anthropic blocks). The envelope carries the SDK
|
|
35
|
+
* correlation fields (session_id / priority / parent_tool_use_id).
|
|
36
|
+
*/
|
|
37
|
+
pushContent(content, envelope) {
|
|
38
|
+
this.push({
|
|
39
|
+
type: "user",
|
|
40
|
+
message: { role: "user", content: sdkMessageContent(content) },
|
|
41
|
+
...envelope,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
close() {
|
|
45
|
+
this.done = true;
|
|
46
|
+
const waiter = this.waiter;
|
|
47
|
+
this.waiter = undefined;
|
|
48
|
+
waiter?.();
|
|
49
|
+
}
|
|
50
|
+
async *[Symbol.asyncIterator]() {
|
|
51
|
+
while (true) {
|
|
52
|
+
const next = this.pending.shift();
|
|
53
|
+
if (next !== undefined) {
|
|
54
|
+
yield next;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (this.done)
|
|
58
|
+
return;
|
|
59
|
+
await new Promise((resolvePromise) => {
|
|
60
|
+
this.waiter = resolvePromise;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|