@deepseek-ai/dsh-headless 0.1.5-rc.2 → 0.1.6-alpha.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/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { n as projectJsonRun, t as boundJsonLine } from "./json-stream-Bh4BGGk9.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import z from "@deepseek-ai/schemastery";
3
4
  import { brandString } from "@deepseek-ai/dsh-brand";
@@ -5,16 +6,95 @@ import { installModelSelection } from "@deepseek-ai/dsh-agent";
5
6
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
6
7
  import { assertNever } from "@deepseek-ai/dsh-util-values";
7
8
  import { SessionSeq } from "@deepseek-ai/dsh-session";
9
+ import { SessionQueryError } from "@deepseek-ai/dsh-session-query";
10
+ //#region lib/types/runner-internals.js
11
+ /**
12
+ * Process streams the runner reads and writes, kept out of the package entry so
13
+ * substituting them in tests adds no public package API. The shape matches the
14
+ * runner's own IO carrier structurally.
15
+ * @module @deepseek-ai/dsh-headless/runner-internals
16
+ */
17
+ /** The process streams the runner reads and writes; tests substitute captures. */
18
+ const internals = {
19
+ stdout: process.stdout,
20
+ stderr: process.stderr,
21
+ readStdin: async () => {
22
+ const chunks = [];
23
+ for await (const chunk of process.stdin) chunks.push(chunk);
24
+ return Buffer.concat(chunks).toString("utf8");
25
+ }
26
+ };
27
+ //#endregion
8
28
  //#region lib/types/index.js
9
29
  /**
10
30
  * @deepseek-ai/dsh-headless — one-shot direct Agent driver. The bundle patch
11
31
  * rides over dsh-base without Host, HTTP, or browser plugins; this runner
12
- * creates one Agent through the core registry, drives the task to quiescence,
13
- * streams provider reasoning to stderr, flushes its Session, prints the final
14
- * assistant text to stdout, and exits.
32
+ * creates one Agent through the core registry (or adopts the exact Session a
33
+ * `--session-id` names), drives the task to quiescence, streams provider
34
+ * reasoning to stderr, flushes its Session, prints the final assistant text to
35
+ * stdout, and exits. With `--json` it projects the run as newline-delimited
36
+ * events instead of the final text.
15
37
  *
16
38
  * @module @deepseek-ai/dsh-headless
17
39
  */
40
+ var __addDisposableResource = function(env, value, async) {
41
+ if (value !== null && value !== void 0) {
42
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
43
+ var dispose, inner;
44
+ if (async) {
45
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
46
+ dispose = value[Symbol.asyncDispose];
47
+ }
48
+ if (dispose === void 0) {
49
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
50
+ dispose = value[Symbol.dispose];
51
+ if (async) inner = dispose;
52
+ }
53
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
54
+ if (inner) dispose = function() {
55
+ try {
56
+ inner.call(this);
57
+ } catch (e) {
58
+ return Promise.reject(e);
59
+ }
60
+ };
61
+ env.stack.push({
62
+ value,
63
+ dispose,
64
+ async
65
+ });
66
+ } else if (async) env.stack.push({ async: true });
67
+ return value;
68
+ };
69
+ var __disposeResources = (function(SuppressedError) {
70
+ return function(env) {
71
+ function fail(e) {
72
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
73
+ env.hasError = true;
74
+ }
75
+ var r, s = 0;
76
+ function next() {
77
+ while (r = env.stack.pop()) try {
78
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
79
+ if (r.dispose) {
80
+ var result = r.dispose.call(r.value);
81
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
82
+ fail(e);
83
+ return next();
84
+ });
85
+ } else s |= 1;
86
+ } catch (e) {
87
+ fail(e);
88
+ }
89
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
90
+ if (env.hasError) throw env.error;
91
+ }
92
+ return next();
93
+ };
94
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
95
+ var e = new Error(message);
96
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
97
+ });
18
98
  /** Stable Cordis plugin name. */
19
99
  const name = "headless-runner";
20
100
  /** Core services required before the one-shot turn can start. */
@@ -23,12 +103,11 @@ const inject = [
23
103
  "agents",
24
104
  "sessions"
25
105
  ];
26
- const Config = z.object({ task: z.string().required() });
27
- /** The process streams the runner writes to; tests substitute captures. */
28
- const internals = {
29
- stdout: process.stdout,
30
- stderr: process.stderr
31
- };
106
+ const Config = z.object({
107
+ task: z.string(),
108
+ sessionId: z.string(),
109
+ json: z.boolean()
110
+ });
32
111
  /** Aggregate the last assistant text and turn outcome in one owned interval. */
33
112
  function summarize(session, firstSeq) {
34
113
  let started = false;
@@ -113,63 +192,165 @@ function streamReasoning(ctx, agent, stderr) {
113
192
  close();
114
193
  };
115
194
  }
195
+ /** Iterate a live Session's durable events in order. */
196
+ function* liveEvents(session) {
197
+ const length = session.seq;
198
+ for (let seq = 0; seq < length; seq++) {
199
+ const event = session.eventAt(SessionSeq(seq));
200
+ if (event === void 0) throw new Error(`headless adoption cannot read seq ${String(seq)} below captured length ${String(length)}`);
201
+ yield event;
202
+ }
203
+ }
204
+ /**
205
+ * The preset a Session currently runs under: its creation header advanced by
206
+ * the last `agent-preset/selected` event. The header is only a creation fact;
207
+ * the presets plugin reconstructs a session's composition from the projection.
208
+ */
209
+ function currentPreset(header, events, sessionId) {
210
+ let preset = header.agentPreset;
211
+ for (const event of events) {
212
+ const candidate = event;
213
+ if (candidate.type !== "agent-preset/selected") continue;
214
+ const selected = candidate.data?.agentPreset;
215
+ if (typeof selected !== "string" || selected === "") throw new Error(`session "${sessionId}" records a malformed agent-preset/selected event and cannot be adopted`);
216
+ preset = selected;
217
+ }
218
+ return preset;
219
+ }
220
+ /** Reject a Session the one-shot runner must not adopt. */
221
+ function assertAdoptable(header, events, sessionId, cwd) {
222
+ const preset = currentPreset(header, events, sessionId);
223
+ if (preset !== void 0) throw new Error(`session "${sessionId}" runs under agent preset "${preset}", which the one-shot runner does not compose`);
224
+ if (header.origin === "subagent" || header.parentSession !== void 0) throw new Error(`session "${sessionId}" is a subagent or forked session and cannot be driven directly`);
225
+ if (header.cwd === void 0) throw new Error(`session "${sessionId}" recorded no working directory, so it cannot be adopted`);
226
+ if (header.cwd !== cwd) throw new Error(`session "${sessionId}" was recorded in "${header.cwd}", not "${cwd}"`);
227
+ }
228
+ /**
229
+ * Resolve the Agent for one run: adopt the persisted Session with the requested
230
+ * id. The identity must already exist, and no Agent may be live under it; a
231
+ * first round omits the option instead, so a typo cannot pass as a brand-new
232
+ * conversation.
233
+ * @param ctx - plugin context carrying the Session query service.
234
+ * @param agents - the core Agent registry.
235
+ * @param sessionId - exact Session identity to adopt.
236
+ * @param agentOptions - provider/model pair for this run.
237
+ * @param setup - per-Agent scope setup installing the model selection.
238
+ * @param cwd - working directory resolved in the mounted filesystem.
239
+ * @returns the resumed Agent.
240
+ */
241
+ async function resolveAgent(ctx, agents, sessionId, agentOptions, setup, cwd) {
242
+ if (ctx.get("sessionPersistence") === void 0) throw new Error("headless --session-id requires the sessionPersistence service; the Session would not survive this process");
243
+ const query = ctx.get("sessionQuery");
244
+ if (query === void 0) throw new Error("headless --session-id requires the sessionQuery service; dsh-base provides it");
245
+ const live = agents.get(sessionId);
246
+ if (live !== void 0) {
247
+ assertAdoptable(live.session.header, liveEvents(live.session), sessionId, cwd);
248
+ throw new Error(`session "${sessionId}" is live in this process, so the one-shot runner cannot own an exclusive run interval`);
249
+ }
250
+ try {
251
+ const env_1 = {
252
+ stack: [],
253
+ error: void 0,
254
+ hasError: false
255
+ };
256
+ try {
257
+ const observation = __addDisposableResource(env_1, await query.observeSession(sessionId), false);
258
+ assertAdoptable(observation.header, observation.events, sessionId, cwd);
259
+ const { agent } = await agents.resume({
260
+ resumeSessionId: sessionId,
261
+ agentOptions,
262
+ setup
263
+ });
264
+ assertAdoptable(agent.session.header, liveEvents(agent.session), sessionId, cwd);
265
+ return agent;
266
+ } catch (e_1) {
267
+ env_1.error = e_1;
268
+ env_1.hasError = true;
269
+ } finally {
270
+ __disposeResources(env_1);
271
+ }
272
+ } catch (error) {
273
+ if (!(error instanceof SessionQueryError) || error.code !== "SESSION_QUERY_SESSION_NOT_FOUND") throw error;
274
+ throw new Error(`session "${sessionId}" does not exist; omit --session-id to start a new Session`);
275
+ }
276
+ }
116
277
  /** Report an unexpected direct-driver failure and request a failing exit. */
117
- function fail(io, error) {
118
- io.stderr.write(`dsh: ${error instanceof Error ? error.message : String(error)}\n`);
278
+ function fail(io, error, json) {
279
+ const message = error instanceof Error ? error.message : String(error);
280
+ if (json) io.stdout.write(`${boundJsonLine({
281
+ type: "error",
282
+ message
283
+ })}\n`);
284
+ io.stderr.write(`dsh: ${message}\n`);
119
285
  io.exit(1);
120
286
  }
121
287
  /**
122
- * Run one task through a freshly created Agent and request process exit.
288
+ * Run one task through one Agent and request process exit.
123
289
  * @param ctx - plugin context carrying the Agent, default model, Session, and launcher IO services.
124
- * @param task - one-shot task text.
290
+ * @param config - task, optional exact Session identity, and output mode.
125
291
  * @param io - process-facing effects.
126
292
  */
127
- async function run(ctx, task, io) {
293
+ async function run(ctx, config, io) {
128
294
  await ctx.get("loader")?.await();
129
295
  const agents = ctx.get("agents");
130
296
  const defaultModel = ctx.get("agentDefaultModel");
131
297
  const sessions = ctx.get("sessions");
132
298
  if (agents === void 0 || defaultModel === void 0 || sessions === void 0) return;
299
+ if (config.sessionId !== void 0 && config.sessionId.trim() === "") throw new Error("headless-runner: sessionId must not be blank");
300
+ const task = config.task === void 0 || config.task === "-" ? await internals.readStdin() : config.task;
301
+ if (task.trim() === "") throw new Error("a task is required, for example: dsh --profile headless \"run the tests\"");
133
302
  const selection = defaultModel.currentSelection();
134
- const { agent } = await agents.create({
135
- sessionId: brandString(`session-${randomUUID()}`),
136
- meta: { cwd: process.cwd() },
137
- agentOptions: {
138
- provider: selection.provider,
139
- model: selection.model
140
- },
141
- setup: (agentCtx) => {
142
- installModelSelection(agentCtx, {
143
- current: selection,
144
- assembled: void 0
145
- });
146
- }
147
- });
303
+ const agentOptions = {
304
+ provider: selection.provider,
305
+ model: selection.model
306
+ };
307
+ const setup = (agentCtx) => {
308
+ installModelSelection(agentCtx, {
309
+ current: selection,
310
+ assembled: void 0
311
+ });
312
+ };
313
+ const sessionId = brandString(config.sessionId ?? `session-${randomUUID()}`);
314
+ const fs = ctx.get("fs");
315
+ const cwd = fs === void 0 ? process.cwd() : fs.processPath(await fs.resolve("."));
316
+ const agent = config.sessionId === void 0 ? (await agents.create({
317
+ sessionId,
318
+ meta: { cwd },
319
+ agentOptions,
320
+ setup
321
+ })).agent : await resolveAgent(ctx, agents, sessionId, agentOptions, setup, cwd);
148
322
  await agent.whenIdle();
323
+ if (config.sessionId !== void 0) assertAdoptable(agent.session.header, liveEvents(agent.session), sessionId, cwd);
149
324
  const firstSeq = agent.session.seq;
150
- const stopReasoning = streamReasoning(ctx, agent, io.stderr);
325
+ const projection = config.json === true ? projectJsonRun(ctx, agent, io.stdout, { cwd }) : void 0;
326
+ const stopReasoning = projection === void 0 ? streamReasoning(ctx, agent, io.stderr) : void 0;
151
327
  try {
152
- agent.followup(createUserMessage({
153
- content: [{
154
- type: "text",
155
- text: task
156
- }],
157
- source: { kind: "user" }
158
- }));
159
- await agent.whenIdle();
328
+ try {
329
+ agent.followup(createUserMessage({
330
+ content: [{
331
+ type: "text",
332
+ text: task
333
+ }],
334
+ source: { kind: "user" }
335
+ }));
336
+ await agent.whenIdle();
337
+ } finally {
338
+ stopReasoning?.();
339
+ }
340
+ await sessions.flush(agent.session);
341
+ const outcome = summarize(agent.session, firstSeq);
342
+ if (projection === void 0) io.stdout.write(outcome.text + "\n");
343
+ else projection.finish(outcome.text);
344
+ if (outcome.reason?.kind === "error") io.stderr.write(`dsh: ${outcome.reason.error.code}: ${outcome.reason.error.message}\n`);
345
+ io.exit(outcome.reason?.kind === "completed" ? 0 : 1);
160
346
  } finally {
161
- stopReasoning();
347
+ projection?.dispose();
162
348
  }
163
- await sessions.flush(agent.session);
164
- const outcome = summarize(agent.session, firstSeq);
165
- io.stdout.write(outcome.text + "\n");
166
- if (outcome.reason?.kind === "error") io.stderr.write(`dsh: ${outcome.reason.error.code}: ${outcome.reason.error.message}\n`);
167
- io.exit(outcome.reason?.kind === "completed" ? 0 : 1);
168
349
  }
169
350
  /**
170
351
  * Mount the one-shot direct driver.
171
352
  * @param ctx - plugin context carrying core services and the launcher-provided exit request.
172
- * @param config - validated task config.
353
+ * @param config - validated task and run options.
173
354
  */
174
355
  function apply(ctx, config) {
175
356
  const exit = ctx.get("appExit");
@@ -179,9 +360,9 @@ function apply(ctx, config) {
179
360
  stderr: internals.stderr,
180
361
  exit
181
362
  };
182
- run(ctx, config.task, io).catch((error) => {
183
- fail(io, error);
363
+ run(ctx, config, io).catch((error) => {
364
+ fail(io, error, config.json === true);
184
365
  });
185
366
  }
186
367
  //#endregion
187
- export { Config, apply, inject, internals, name };
368
+ export { Config, apply, inject, name };
@@ -0,0 +1,280 @@
1
+ import { lastAssistantStreamChunk } from "@deepseek-ai/dsh-llm/assistant-stream";
2
+ //#region lib/types/json-stream.js
3
+ /**
4
+ * `--json` run projection: a bounded, ordered event stream derived from one
5
+ * Agent's durable Session events. Every projected event is a commit point:
6
+ * text and reasoning come from committed `assistant/message` content, never
7
+ * from a live attempt that may still be retried or discarded, so the stream
8
+ * never carries content the durable log does not contain.
9
+ * @module @deepseek-ai/dsh-headless/json-stream
10
+ */
11
+ /** Default per-string and per-key cap applied to every bounded projected payload. */
12
+ const MAX_STRING_BYTES = 8 * 1024;
13
+ /** Default cap on one projected event's serialized bytes, newline included; the terminal `final` is exempt. */
14
+ const MAX_EVENT_BYTES = 32 * 1024;
15
+ /** Bytes every newline-delimited writer appends after one serialized event. */
16
+ const LINE_TERMINATOR_BYTES = 1;
17
+ /** Truncate one UTF-8 string to `maxBytes`, dropping a split trailing character. */
18
+ function truncateUtf8(text, maxBytes) {
19
+ const decoded = Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
20
+ return decoded.endsWith("�") ? decoded.slice(0, -1) : decoded;
21
+ }
22
+ /** Cap one object key, flagging the payload when it was cut. */
23
+ function boundKey(key, maxBytes, state) {
24
+ if (Buffer.byteLength(key, "utf8") <= maxBytes) return key;
25
+ state.truncated = true;
26
+ return truncateUtf8(key, maxBytes);
27
+ }
28
+ /** Maximum container depth one projected payload keeps before the tail is cut. */
29
+ const MAX_DEPTH = 64;
30
+ /** Recursively cap every string in one JSON-serializable value, keys included. */
31
+ function boundValue(value, maxBytes, state, depth = 0) {
32
+ if (typeof value === "string") {
33
+ if (Buffer.byteLength(value, "utf8") <= maxBytes) return value;
34
+ state.truncated = true;
35
+ return truncateUtf8(value, maxBytes);
36
+ }
37
+ if (Array.isArray(value)) {
38
+ if (depth >= MAX_DEPTH) {
39
+ state.truncated = true;
40
+ return "[truncated: depth]";
41
+ }
42
+ return value.map((item) => boundValue(item, maxBytes, state, depth + 1));
43
+ }
44
+ if (value !== null && typeof value === "object") {
45
+ if (depth >= MAX_DEPTH) {
46
+ state.truncated = true;
47
+ return "[truncated: depth]";
48
+ }
49
+ const bounded = Object.create(null);
50
+ for (const [key, item] of Object.entries(value)) bounded[boundKey(key, maxBytes, state)] = boundValue(item, maxBytes, state, depth + 1);
51
+ return bounded;
52
+ }
53
+ return value;
54
+ }
55
+ /**
56
+ * Bound every string and key in one projected payload, adding `truncated: true`
57
+ * when any was cut. {@link boundJsonLine} composes this with the whole-line cap.
58
+ * @param event - the event payload to bound.
59
+ * @param maxStringBytes - per-string and per-key byte cap.
60
+ * @returns a copy with every over-long string and key truncated.
61
+ */
62
+ function boundJsonEvent(event, maxStringBytes = MAX_STRING_BYTES) {
63
+ const state = { truncated: false };
64
+ const bounded = boundValue(event, maxStringBytes, state);
65
+ if (state.truncated) bounded.truncated = true;
66
+ return bounded;
67
+ }
68
+ /**
69
+ * Serialize one projected payload under both limits: every string and key is
70
+ * capped at `maxStringBytes`, and the serialized line at `maxEventBytes`. The
71
+ * line cap reserves the newline the writer appends, so the complete record
72
+ * stays within it. When the line is still too long, scalar fields survive and
73
+ * structured fields are dropped; when even those are too long, only `type` and
74
+ * `truncated` remain.
75
+ * @param event - the event payload to serialize.
76
+ * @param maxStringBytes - per-string and per-key byte cap.
77
+ * @param maxEventBytes - cap on the serialized line plus its trailing newline.
78
+ * @returns the bounded JSON line, without a trailing newline.
79
+ */
80
+ function boundJsonLine(event, maxStringBytes = MAX_STRING_BYTES, maxEventBytes = MAX_EVENT_BYTES) {
81
+ const limit = maxEventBytes - LINE_TERMINATOR_BYTES;
82
+ const bounded = boundJsonEvent(event, maxStringBytes);
83
+ const line = JSON.stringify(bounded);
84
+ if (Buffer.byteLength(line, "utf8") <= limit) return line;
85
+ const scalars = Object.create(null);
86
+ for (const [key, value] of Object.entries(bounded)) if (value === null || typeof value !== "object") scalars[key] = value;
87
+ scalars.truncated = true;
88
+ const short = JSON.stringify(scalars);
89
+ if (Buffer.byteLength(short, "utf8") <= limit) return short;
90
+ return JSON.stringify({
91
+ type: bounded.type,
92
+ truncated: true
93
+ });
94
+ }
95
+ /**
96
+ * Parse raw tool-call arguments as the executor does: empty input is `{}`,
97
+ * invalid or non-round-trippable JSON (a non-finite number) stays text.
98
+ */
99
+ function parseArguments(raw) {
100
+ if (raw === "") return {};
101
+ const seen = { nonFinite: false };
102
+ try {
103
+ const parsed = JSON.parse(raw, (_key, value) => {
104
+ if (typeof value === "number" && !Number.isFinite(value)) seen.nonFinite = true;
105
+ return value;
106
+ });
107
+ return seen.nonFinite ? raw : parsed;
108
+ } catch {
109
+ return raw;
110
+ }
111
+ }
112
+ /** Join the text blocks of a tool result's model-facing content. */
113
+ function resultText(blocks) {
114
+ return blocks.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("");
115
+ }
116
+ /** The usage a provider reported in one attempt's stream, if any. */
117
+ function streamUsage(stream) {
118
+ return lastAssistantStreamChunk(stream, "usage")?.usage;
119
+ }
120
+ /**
121
+ * Accumulate one step's usage across its attempts. A retried attempt keeps its
122
+ * usage only in its `assistant/attempt` stream, so the committed message alone
123
+ * would under-report billed tokens. An attempt that reports no sample makes the
124
+ * step total unknowable, so the step omits usage entirely rather than publish a
125
+ * partial sum as if it were complete; an optional bucket is summed only when
126
+ * every contribution reports it.
127
+ * @param state - usage accumulated so far and whether it is still complete.
128
+ * @param next - usage reported by the next attempt.
129
+ * @returns the updated state.
130
+ */
131
+ function addUsage(state, next) {
132
+ if (next === void 0) return {
133
+ usage: state.usage,
134
+ complete: false
135
+ };
136
+ if (state.usage === void 0) return {
137
+ usage: next,
138
+ complete: state.complete
139
+ };
140
+ const total = state.usage;
141
+ const sum = (a, b) => a === void 0 || b === void 0 ? void 0 : a + b;
142
+ const totalTokens = sum(total.totalTokens, next.totalTokens);
143
+ const cacheReadTokens = sum(total.cacheReadTokens, next.cacheReadTokens);
144
+ const cacheWriteTokens = sum(total.cacheWriteTokens, next.cacheWriteTokens);
145
+ const reasoningTokens = sum(total.reasoningTokens, next.reasoningTokens);
146
+ return {
147
+ usage: {
148
+ inputTokens: total.inputTokens + next.inputTokens,
149
+ outputTokens: total.outputTokens + next.outputTokens,
150
+ ...totalTokens === void 0 ? {} : { totalTokens },
151
+ ...cacheReadTokens === void 0 ? {} : { cacheReadTokens },
152
+ ...cacheWriteTokens === void 0 ? {} : { cacheWriteTokens },
153
+ ...reasoningTokens === void 0 ? {} : { reasoningTokens }
154
+ },
155
+ complete: state.complete
156
+ };
157
+ }
158
+ /**
159
+ * Project one Agent's run as newline-delimited JSON on `sink`.
160
+ *
161
+ * The opening `session` event is written before the subscription starts, so a
162
+ * caller must invoke this before submitting the task. Text and reasoning are
163
+ * emitted only when the step's `assistant/message` commits them, and the
164
+ * terminal `final` event carries the same lossless answer the default mode
165
+ * prints (it is deliberately not bounded).
166
+ * @param ctx - plugin context carrying the live Session feed.
167
+ * @param agent - the exact Agent whose events belong to this invocation.
168
+ * @param sink - stdout sink receiving one JSON object per line.
169
+ * @param options - projection tunables.
170
+ * @returns the projection handle that finishes or disposes the stream.
171
+ */
172
+ function projectJsonRun(ctx, agent, sink, options = {}) {
173
+ const maxStringBytes = options.maxStringBytes ?? 8192;
174
+ let disposed = false;
175
+ let stepUsage = {
176
+ usage: void 0,
177
+ complete: true
178
+ };
179
+ const write = (event) => {
180
+ sink.write(`${boundJsonLine(event, maxStringBytes)}\n`);
181
+ };
182
+ const onSessionEvent = (session, event) => {
183
+ if (session !== agent.session) return;
184
+ switch (event.type) {
185
+ case "turn/start":
186
+ write({
187
+ type: "status",
188
+ phase: "turn_start",
189
+ turn: event.data.turn
190
+ });
191
+ return;
192
+ case "step/start":
193
+ write({
194
+ type: "status",
195
+ phase: "step_start",
196
+ turn: event.data.turn,
197
+ step: event.data.step
198
+ });
199
+ return;
200
+ case "assistant/attempt":
201
+ stepUsage = addUsage(stepUsage, streamUsage(event.data.stream));
202
+ return;
203
+ case "assistant/message":
204
+ stepUsage = addUsage(stepUsage, event.data.usage ?? streamUsage(event.data.stream));
205
+ for (const block of event.data.message.content) if (block.type === "reasoning") write({
206
+ type: "thinking",
207
+ text: block.text
208
+ });
209
+ else if (block.type === "text") write({
210
+ type: "text",
211
+ text: block.text
212
+ });
213
+ return;
214
+ case "step/end": {
215
+ const { usage, complete } = stepUsage;
216
+ stepUsage = {
217
+ usage: void 0,
218
+ complete: true
219
+ };
220
+ write({
221
+ type: "status",
222
+ phase: "step_end",
223
+ turn: event.data.turn,
224
+ step: event.data.step,
225
+ ...complete && usage !== void 0 ? { usage } : {}
226
+ });
227
+ return;
228
+ }
229
+ case "turn/end":
230
+ write({
231
+ type: "status",
232
+ phase: "turn_end",
233
+ turn: event.data.turn,
234
+ reason: event.data.reason
235
+ });
236
+ return;
237
+ case "tool/call":
238
+ write({
239
+ type: "tool_call",
240
+ callId: event.data.callId,
241
+ tool: event.data.name,
242
+ input: parseArguments(event.data.arguments)
243
+ });
244
+ return;
245
+ case "tool/result": {
246
+ if (event.surfaceOp !== "append") return;
247
+ const block = event.data.message.content[0];
248
+ write({
249
+ type: "tool_result",
250
+ callId: block.toolCallId,
251
+ status: block.isError === true ? "error" : "completed",
252
+ result: resultText(block.content)
253
+ });
254
+ return;
255
+ }
256
+ default: return;
257
+ }
258
+ };
259
+ write({
260
+ type: "session",
261
+ sessionId: agent.id,
262
+ cwd: options.cwd ?? process.cwd()
263
+ });
264
+ const stopSession = ctx.on("session/event", onSessionEvent);
265
+ return {
266
+ finish(text) {
267
+ if (disposed) return;
268
+ sink.write(`${JSON.stringify({
269
+ type: "final",
270
+ text
271
+ })}\n`);
272
+ },
273
+ dispose() {
274
+ disposed = true;
275
+ stopSession();
276
+ }
277
+ };
278
+ }
279
+ //#endregion
280
+ export { projectJsonRun as n, boundJsonLine as t };