@tangle-network/sdk-provider-cli-base 0.13.16 → 0.13.18

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.
@@ -0,0 +1,240 @@
1
+ import { Ct as finalTextEvent, H as CliProviderAdapterBase, Tt as sessionObservedEvents, V as recordCliTokenUsage, ot as prependPromptSection, x as nativeEffortArgs } from "../../src-Fn8Pu1xq.mjs";
2
+ import { resolveTangleRouterDefaults } from "@tangle-network/agent-interface";
3
+ //#region src/adapters/openclaw/adapter.ts
4
+ /** openclaw's default agent id; the synthesized config declares only it. */
5
+ const OPENCLAW_AGENT_ID = "main";
6
+ /**
7
+ * OpenClaw's embedded agent: `openclaw agent --local --json --agent main
8
+ * --session-id <id> -m "<prompt>"`, configured by `~/.openclaw/openclaw.json`.
9
+ * It does not stream: `--json` prints one envelope when the turn ends, and
10
+ * that envelope is the only place it reports usage and its session id.
11
+ */
12
+ var OpenclawProviderAdapter = class extends CliProviderAdapterBase {
13
+ provider = "openclaw";
14
+ profileHarness = "openclaw";
15
+ defaultCliPath = "openclaw";
16
+ /**
17
+ * Managed `~/.openclaw/openclaw.json` synthesized during initialize() and
18
+ * materialized to disk by the sidecar via getManagedAuthFiles(). Empty when
19
+ * the caller manages openclaw config itself.
20
+ */
21
+ managedAuthFiles = [];
22
+ /**
23
+ * Openclaw reads NO credentials from env — it loads them from
24
+ * `~/.openclaw/openclaw.json`. A managed sandbox provisions none, so without
25
+ * a synthesized config openclaw falls back to its bundled `claude-cli`
26
+ * backend (which needs a Claude login) and the session never starts. Always
27
+ * synthesize a router-backed config so `openclaw agent --local` authenticates
28
+ * against the Tangle router, unless the caller manages config itself
29
+ * (authMode "config-only") or already supplied an `openclaw.json`.
30
+ */
31
+ async afterInitialize(_config) {
32
+ this.managedAuthFiles = this.computeManagedAuthFiles();
33
+ }
34
+ getManagedAuthFiles() {
35
+ return this.managedAuthFiles;
36
+ }
37
+ computeManagedAuthFiles() {
38
+ if (this.config.authMode === "config-only") return [];
39
+ if ((this.config.authFiles ?? []).some((f) => f.path === ".openclaw/openclaw.json")) return [];
40
+ this.logger.debug("openclaw: synthesized managed router config");
41
+ return [{
42
+ path: ".openclaw/openclaw.json",
43
+ content: buildRouterOpenclawConfig(),
44
+ mode: 384
45
+ }];
46
+ }
47
+ buildExecArgs(params) {
48
+ const args = ["agent"];
49
+ args.push("--local");
50
+ args.push("--json");
51
+ args.push("--agent", OPENCLAW_AGENT_ID);
52
+ const nativeSessionId = params.sessionId ?? params.mintedSessionId;
53
+ if (nativeSessionId) args.push("--session-id", validateOpenclawSessionId(nativeSessionId));
54
+ args.push(...nativeEffortArgs("openclaw", params.config.profile?.model?.reasoningEffort));
55
+ if (params.config.extraFlags?.length) args.push(...params.config.extraFlags);
56
+ const fullMessage = prependPromptSection(params.systemPrompt, params.message);
57
+ args.push("-m", fullMessage);
58
+ return {
59
+ command: this.getCliPath(),
60
+ args,
61
+ useStdin: false
62
+ };
63
+ }
64
+ buildExecEnv(config) {
65
+ const env = this.agentEnvironmentValues();
66
+ env.OPENCLAW_NONINTERACTIVE = "1";
67
+ if (this.managedAuthFiles.length > 0) {
68
+ env.OPENAI_API_KEY = void 0;
69
+ env.ANTHROPIC_API_KEY = void 0;
70
+ env.OPENROUTER_API_KEY = void 0;
71
+ env.GEMINI_API_KEY = void 0;
72
+ env.GOOGLE_API_KEY = void 0;
73
+ const managedConfig = this.managedAuthFiles.find((f) => f.path.endsWith(".openclaw/openclaw.json"));
74
+ if (managedConfig && !env.OPENCLAW_CONFIG_PATH) env.OPENCLAW_CONFIG_PATH = `${(config.runtimeHome?.trim() || env.SIDECAR_AGENT_HOME?.trim() || env.AGENT_WORKSPACE_ROOT?.trim() || "/home/agent").replace(/\/+$/, "")}/${managedConfig.path}`;
75
+ }
76
+ return env;
77
+ }
78
+ /**
79
+ * openclaw's file and exec tools resolve against its agent workspace
80
+ * (`~/.openclaw/workspace` by default), not the process cwd, so a turn that
81
+ * did not pin it edited a directory the caller never sees.
82
+ */
83
+ buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders) {
84
+ return {
85
+ ...super.buildTurnEnvironment(executionConfig, profilePlan, workspaceRoot, correlationHeaders),
86
+ OPENCLAW_WORKSPACE_DIR: workspaceRoot
87
+ };
88
+ }
89
+ createEventParser(state, onEvent) {
90
+ const ext = state;
91
+ ext.stdoutBuffer = "";
92
+ return {
93
+ parseAs: "raw",
94
+ onEvent: () => {},
95
+ onComplete: (rawBuffer) => {
96
+ ext.stdoutBuffer = rawBuffer ?? "";
97
+ for (const stream of this.flushEnvelope(ext)) onEvent?.(stream);
98
+ }
99
+ };
100
+ }
101
+ /**
102
+ * Parse the buffered stdout as openclaw's JSON envelope and synthesize the
103
+ * canonical message.part.updated + status:completed events.
104
+ */
105
+ flushEnvelope(state) {
106
+ const trimmed = state.stdoutBuffer.trim();
107
+ if (!trimmed) return [];
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(trimmed);
111
+ } catch (err) {
112
+ return [{
113
+ type: "warning",
114
+ code: "OPENCLAW_PARSE_ERROR",
115
+ message: `Failed to parse openclaw envelope: ${err instanceof Error ? err.message : String(err)}`
116
+ }];
117
+ }
118
+ const text = extractFinalText(parsed);
119
+ const agentMeta = extractAgentMetadata(parsed);
120
+ const sessionId = typeof agentMeta.sessionId === "string" ? agentMeta.sessionId : void 0;
121
+ const events = sessionId ? sessionObservedEvents(state, "openclaw", sessionId) : [];
122
+ recordCliTokenUsage(state, agentMeta);
123
+ const textEvent = finalTextEvent(state, text);
124
+ if (text) events.push(textEvent);
125
+ events.push({
126
+ type: "raw",
127
+ backend: this.provider,
128
+ event: {
129
+ provider: agentMeta.provider,
130
+ model: agentMeta.model
131
+ }
132
+ });
133
+ events.push({
134
+ type: "status",
135
+ status: "completed"
136
+ });
137
+ return events;
138
+ }
139
+ };
140
+ /**
141
+ * Walk an arbitrary content node and accumulate every string-valued `text`
142
+ * field plus any direct strings. Bounded depth (default 4) prevents
143
+ * runaway recursion on pathologically nested envelopes — OpenAI/Anthropic
144
+ * shapes top out at ~2 levels of nesting in practice.
145
+ *
146
+ * Recognized shapes (real-world, from production CLIs that openclaw routes to):
147
+ * "plain string"
148
+ * { text: "..." }
149
+ * { type: "text", text: "..." } // OpenAI message content
150
+ * [{ type: "text", text: "..." }, ...] // Anthropic blocks
151
+ * { content: ... } // wrapper object
152
+ * { message: { content: ... } } // OpenAI choices.message
153
+ *
154
+ * Anything that doesn't match (numbers, booleans, tool-call descriptors)
155
+ * is silently skipped — extracting *partial* text is better than throwing.
156
+ */
157
+ function walkContentForText(raw, parts, depth = 4) {
158
+ if (depth <= 0 || raw == null) return;
159
+ if (typeof raw === "string") {
160
+ parts.push(raw);
161
+ return;
162
+ }
163
+ if (Array.isArray(raw)) {
164
+ for (const item of raw) walkContentForText(item, parts, depth - 1);
165
+ return;
166
+ }
167
+ if (typeof raw === "object") {
168
+ const obj = raw;
169
+ if (typeof obj.text === "string") {
170
+ parts.push(obj.text);
171
+ return;
172
+ }
173
+ if (obj.content !== void 0) {
174
+ walkContentForText(obj.content, parts, depth - 1);
175
+ return;
176
+ }
177
+ if (obj.message !== void 0) walkContentForText(obj.message, parts, depth - 1);
178
+ }
179
+ }
180
+ function extractFinalText(envelope) {
181
+ const payloads = envelope.payloads;
182
+ if (!Array.isArray(payloads)) return "";
183
+ const parts = [];
184
+ for (const payload of payloads) walkContentForText(payload, parts);
185
+ return parts.join("\n").trim();
186
+ }
187
+ /** Native agent --json metadata lives beside the reply payloads. */
188
+ function extractAgentMetadata(envelope) {
189
+ const meta = envelope.meta;
190
+ if (!meta || typeof meta !== "object") return {};
191
+ const agentMeta = meta.agentMeta;
192
+ return agentMeta && typeof agentMeta === "object" ? agentMeta : {};
193
+ }
194
+ /**
195
+ * Tight charset validator for openclaw session ids, which interpolate into
196
+ * filesystem paths under ~/.openclaw/. Rejects traversal sequences, shell
197
+ * metacharacters, whitespace and NULs. Allows lowercase alphanumerics, dash,
198
+ * underscore and colon (session keys look like `agent:main:abc123`), capped
199
+ * at 128 characters.
200
+ */
201
+ function validateOpenclawSessionId(value) {
202
+ if (!/^[a-z0-9][a-z0-9_:-]{0,127}$/.test(value)) throw new Error(`openclaw: invalid session id ${JSON.stringify(value)} — must match /^[a-z0-9][a-z0-9_:-]{0,127}$/`);
203
+ return value;
204
+ }
205
+ /**
206
+ * The `~/.openclaw/openclaw.json` that points openclaw's default agent at the
207
+ * managed Tangle router, declared as the `openai-completions` provider
208
+ * `tangle`. Its apiKey is an env template openclaw expands at load from the
209
+ * salted per-sandbox key; the egress proxy swaps that for the real
210
+ * budget-scoped key, so a managed session needs no per-sub-CLI credentials.
211
+ * The shape follows openclaw's config schema: top-level `models`/`agents`,
212
+ * and agent records carry a `model` ref.
213
+ */
214
+ function buildRouterOpenclawConfig() {
215
+ const router = resolveTangleRouterDefaults();
216
+ const modelRef = `tangle/${router.model}`;
217
+ const config = {
218
+ models: { providers: { tangle: {
219
+ baseUrl: router.baseUrl,
220
+ apiKey: `\${${router.apiKeyEnvVar}}`,
221
+ api: "openai-completions",
222
+ models: [{
223
+ id: router.model,
224
+ name: router.model
225
+ }]
226
+ } } },
227
+ agents: {
228
+ defaults: {
229
+ model: { primary: modelRef },
230
+ skipBootstrap: true
231
+ },
232
+ entries: { [OPENCLAW_AGENT_ID]: { model: modelRef } }
233
+ }
234
+ };
235
+ return `${JSON.stringify(config, null, 2)}\n`;
236
+ }
237
+ //#endregion
238
+ export { OpenclawProviderAdapter };
239
+
240
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1,126 @@
1
+ import { At as AcpTurn, Ct as AcpBridgeConfig, dn as AcpBridgeSpawn, fn as AcpProcessSpec, mt as AcpProviderAdapterBase, wt as AcpContentBlock } from "../../index-BpO3fvbf.mjs";
2
+ import { i as PreparedAttachment } from "../../prompt-input-CD7okX8T.mjs";
3
+ import { PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, PrimeHarnessStateError, PrimeMaterializeOptions, PrimeProfileControls, PrimeProfileControls as PrimeProfileControls$1, PrimeProfileError, PrimeProfileSkill, PrimeProfileSubagent, assertPrimeModelAgreement, hasPrimeProfileMaterial, matchPrimeKernelDeadMarker, materializePrimeProfileControls, primeAgentDirEnv, primeProfileNeedsAgentDir, primeSubagentEntryId as subagentEntryId, primeThinkingLevel, prunePrimeProfileMaterial, readPrimeProfileControls } from "@tangle-network/agent-profile-materialize";
4
+ import { RpcBridge } from "@tangle-network/sdk-cli-runner";
5
+ import { AgentExecutionInput, BackendCapabilities, ProviderConfig, StreamEvent } from "@tangle-network/agent-interface";
6
+ import { MessagePart } from "@tangle-network/sdk-session-persistence";
7
+
8
+ //#region src/adapters/prime/types.d.ts
9
+ /**
10
+ * prime-agent settings. The ACP process settings (protocol version, prompt
11
+ * and bootstrap timeouts, idle reaping) come from {@link AcpBridgeConfig};
12
+ * prime's bootstrap can include first-run kernel venv provisioning, and a
13
+ * wedged kernel can leave a prompt silent until its timeout. The agent does
14
+ * not populate `rawOutput`: tool output arrives as tool-call content blocks.
15
+ */
16
+ interface PrimeConfig extends AcpBridgeConfig {
17
+ /**
18
+ * Provider key written into the materialized `models.json` and passed as
19
+ * `--provider`. Purely a namespace inside the per-session agent dir.
20
+ * Default: "tangle".
21
+ */
22
+ modelProvider?: string;
23
+ /** Context window advertised for the materialized model entry. */
24
+ contextWindow?: number;
25
+ /** Max output tokens advertised for the materialized model entry. */
26
+ maxTokens?: number;
27
+ /** Whether the materialized model entry is reasoning-capable. */
28
+ reasoningModel?: boolean;
29
+ /**
30
+ * Share ONE prime agent dir (models.json, settings, sessions, and the
31
+ * self-modifying `harness/harness_state.json`) across every session of this
32
+ * adapter instance instead of the per-session default.
33
+ *
34
+ * prime-agent's harness state is persistent self-modification that rides
35
+ * every future system prompt — cross-session it is a prompt-injection
36
+ * channel, so isolation is the fail-closed default and this flag is the
37
+ * explicit opt-in for continual-mode experiments.
38
+ */
39
+ continualHarnessState?: boolean;
40
+ }
41
+ /**
42
+ * Typed session-fatal error for a dead IPython kernel. Callers must treat the
43
+ * session as unrecoverable and start a new one; the adapter has already
44
+ * closed and evicted the backing prime-agent process when this throws.
45
+ */
46
+ declare class PrimeKernelDeadError extends Error {
47
+ readonly sessionId: string;
48
+ readonly marker: string;
49
+ readonly reason = "harness_exit";
50
+ constructor(sessionId: string, marker: string);
51
+ }
52
+ //#endregion
53
+ //#region src/adapters/prime/adapter.d.ts
54
+ /** Test seam — production always spawns via sdk-cli-runner's spawnRpcBridge. */
55
+ declare function __setPrimeBridgeFactoryForTesting(factory: ((request: AcpBridgeSpawn) => RpcBridge) | null): void;
56
+ /** A prime turn: the validated model tuple and the profile controls it runs. */
57
+ interface PrimeTurn extends AcpTurn<PrimeConfig> {
58
+ readonly modelId: string;
59
+ readonly profileControls: PrimeProfileControls$1;
60
+ }
61
+ /**
62
+ * Prime Agent provider adapter.
63
+ *
64
+ * Spawns `prime-agent --mode acp` (JSON-RPC 2.0 over NDJSON on stdio — no
65
+ * TUI; the fork internally routes the session through a client-owned daemon
66
+ * worker that inherits this spawn's env, see types.ts). The ACP session
67
+ * lifecycle is the shared {@link AcpProviderAdapterBase}; this adapter owns
68
+ * prime's process, model tuple and failure signals.
69
+ *
70
+ * Each config directory owns its model tuple, daemon and harness state.
71
+ * Kernel death closes the session with a typed failure.
72
+ *
73
+ * prime hosts one session per connection and has no `session/load`: a new
74
+ * process opens a new ACP session, and the conversation resumes from prime's
75
+ * own session files in the per-session agent dir.
76
+ */
77
+ declare class PrimeProviderAdapter extends AcpProviderAdapterBase<PrimeConfig, PrimeTurn> {
78
+ readonly provider = "prime";
79
+ protected readonly defaultCliPath = "prime-agent";
80
+ protected readonly loadsNativeSessions = false;
81
+ /** A dead kernel closes the ACP process and produces a typed turn failure. */
82
+ private readonly kernelDeaths;
83
+ protected resolveTurn(input: AgentExecutionInput): PrimeTurn;
84
+ protected processSpec(turn: PrimeTurn, context: {
85
+ sessionId: string;
86
+ workspaceRoot: string;
87
+ }): Promise<AcpProcessSpec>;
88
+ protected spawnFailureMessage(failure: {
89
+ message: string;
90
+ missingBinary: boolean;
91
+ }): string;
92
+ protected spawnBridge(request: AcpBridgeSpawn): RpcBridge;
93
+ /**
94
+ * A failed tool whose output carries a kernel-death marker (the ipython
95
+ * tool, or a bootstrap diagnostic) ends the session: record it and close
96
+ * the process so the in-flight prompt can never hang on a dead kernel.
97
+ */
98
+ protected onToolFailed(sessionId: string, failureText: string | undefined): StreamEvent[];
99
+ protected takeTurnFailure(sessionId: string): Error | undefined;
100
+ getCapabilities(): BackendCapabilities;
101
+ /** Prime accepts ACP image blocks, so images are forwarded natively. */
102
+ protected nativeAttachmentKinds(): ReadonlyArray<"image" | "file">;
103
+ protected afterInitialize(_config: ProviderConfig): Promise<void>;
104
+ /** Continual mode explicitly shares the native config and harness state. */
105
+ protected resolveAgentDir(config: PrimeConfig, sessionId: string): string;
106
+ private agentDirsRoot;
107
+ /** The dir every agent dir of this adapter is created below. */
108
+ private agentDirOwnerRoot;
109
+ protected buildSpawnEnv(agentDir: string): Record<string, string>;
110
+ protected buildSpawnArgs(config: PrimeConfig, modelId: string, profileArgs?: readonly string[]): string[];
111
+ private modelKey;
112
+ /**
113
+ * ACP prompt blocks for a turn: the composed text plus every natively
114
+ * deliverable image. An image that could not be inlined (over budget,
115
+ * unreadable, remote-url) falls back to its text marker so the model still
116
+ * sees the reference instead of silently losing it.
117
+ */
118
+ protected promptBlocks(input: AgentExecutionInput, preparedInput: {
119
+ promptText: string;
120
+ persistedParts: MessagePart[];
121
+ attachments: PreparedAttachment[];
122
+ }, turn: PrimeTurn): AcpContentBlock[];
123
+ }
124
+ //#endregion
125
+ export { PRIME_AGENT_DIR_ENV_VAR, PRIME_AGENT_DIR_ENV_VARS, PRIME_HARNESS_STATE_ENV_DENYLIST, PRIME_KERNEL_DEAD_MARKERS, PRIME_KERNEL_ENV_PASSTHROUGH, PRIME_MCP_UNSUPPORTED_REASON, type PrimeConfig, PrimeHarnessStateError, PrimeKernelDeadError, type PrimeMaterializeOptions, type PrimeProfileControls, PrimeProfileError, type PrimeProfileSkill, type PrimeProfileSubagent, PrimeProviderAdapter, __setPrimeBridgeFactoryForTesting, assertPrimeModelAgreement, hasPrimeProfileMaterial, matchPrimeKernelDeadMarker, materializePrimeProfileControls, primeAgentDirEnv, primeProfileNeedsAgentDir, primeThinkingLevel, prunePrimeProfileMaterial, readPrimeProfileControls, subagentEntryId };
126
+ //# sourceMappingURL=index.d.mts.map