@glubean/port 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -86,6 +86,26 @@ const result = await port.turns.startStructured({
86
86
  console.log(result.data)
87
87
  ```
88
88
 
89
+ Streaming partial JSON: when callers want progressive structure (rather than
90
+ the all-or-nothing `startStructured` contract), accumulate `message.delta.text`
91
+ and feed it to a partial-JSON parser. [`partial-json`](https://www.npmjs.com/package/partial-json)
92
+ is a zero-dependency option designed for LLM streaming:
93
+
94
+ ```ts
95
+ import { parse, Allow } from "partial-json"
96
+
97
+ let accumulated = ""
98
+ for await (const event of port.turns.start({ sessionId: session.id, input })) {
99
+ if (event.type !== "message.delta") continue
100
+ accumulated += event.text
101
+ const partial = parse(accumulated, Allow.ALL)
102
+ // push `partial` to the UI as it grows
103
+ }
104
+ ```
105
+
106
+ Use `startStructured` when you need final-shape validation or retry on malformed
107
+ output; use the partial-JSON pattern when you need to render incrementally.
108
+
89
109
  ## Local Development
90
110
 
91
111
  Node 24 can run the TypeScript sources directly.
package/dist/lifecycle.js CHANGED
@@ -104,6 +104,10 @@ class LifecycleRuntime {
104
104
  const ref = eventToRef(runtime.#adapter.id, event, input.sessionId);
105
105
  if (ref && !currentTurnId)
106
106
  currentTurnId = ref.turnId;
107
+ // Set terminalSeen BEFORE yielding so that if the consumer
108
+ // breaks on the terminal event, the finally below does not
109
+ // append a spurious cancellation. Same reasoning applies to
110
+ // the synthesized terminals further down.
107
111
  if (event.type === "turn.completed")
108
112
  terminalSeen = true;
109
113
  runtime.#record(event, input);
@@ -111,6 +115,7 @@ class LifecycleRuntime {
111
115
  }
112
116
  if (currentTurnId && !terminalSeen) {
113
117
  const completed = completedEvent(input.sessionId, currentTurnId, "completed");
118
+ terminalSeen = true;
114
119
  runtime.#record(completed, input);
115
120
  yield completed;
116
121
  }
@@ -120,6 +125,7 @@ class LifecycleRuntime {
120
125
  if (currentTurnId) {
121
126
  const errorEvent = { type: "error", sessionId: input.sessionId, turnId: currentTurnId, message };
122
127
  const completed = completedEvent(input.sessionId, currentTurnId, "failed", message);
128
+ terminalSeen = true;
123
129
  runtime.#record(errorEvent, input);
124
130
  runtime.#record(completed, input);
125
131
  yield errorEvent;
@@ -127,6 +133,26 @@ class LifecycleRuntime {
127
133
  }
128
134
  throw error;
129
135
  }
136
+ finally {
137
+ // If the consumer abandons the iterator after the turn was accepted
138
+ // but before a terminal event, synthesize a cancellation so
139
+ // turns.status / turns.wait don't report "running" forever, and
140
+ // ALWAYS poke the adapter to stop work for that turn. We can't
141
+ // tell from inside lifecycle whether a previously-recorded error
142
+ // event was a terminal failure (claude spawn ENOENT) or a benign
143
+ // diagnostic (codex/gemini stderr surfaced as error) — so we
144
+ // treat the consumer's break as the authoritative termination
145
+ // signal, preserving the recorded error in the snapshot for
146
+ // informational purposes.
147
+ if (currentTurnId && !terminalSeen) {
148
+ const recorded = runtime.#turns.get(turnKey(input.sessionId, currentTurnId));
149
+ if (!recorded || !isTerminalStatus(recorded.status)) {
150
+ const cancelled = completedEvent(input.sessionId, currentTurnId, "cancelled", recorded?.error);
151
+ runtime.#record(cancelled, input);
152
+ void runtime.#adapter.cancelTurn({ sessionId: input.sessionId, turnId: currentTurnId }).catch(() => undefined);
153
+ }
154
+ }
155
+ }
130
156
  },
131
157
  };
132
158
  }
@@ -1 +1 @@
1
- {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/providers/claude.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAGV,aAAa,EACb,kBAAkB,EAGlB,IAAI,EACJ,cAAc,EAEd,cAAc,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,KAAK,iBAAiB,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;AAEtD,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,QAAQ,CAwCtD,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAE1F;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAGxG;AA0ID,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,CAe9G"}
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../../src/providers/claude.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAGV,aAAa,EACb,kBAAkB,EAGlB,IAAI,EACJ,cAAc,EAEd,cAAc,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,KAAK,iBAAiB,GAAG,kBAAkB,CAAC,QAAQ,CAAC,CAAC;AAEtD,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,QAAQ,CAwCtD,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAE1F;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,CAGxG;AA2KD,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,EAAE,cAAc,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,CAe9G"}
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { EventEmitter } from "node:events";
4
4
  import process from "node:process";
5
+ import { AsyncQueue } from "../async-queue.js";
5
6
  import { createPortFromAdapter } from "../port.js";
6
7
  import { providerRuntimeOptions, resolveRuntimeOptions } from "../options.js";
7
8
  import { normalizeTokenUsage } from "../token-usage.js";
@@ -93,14 +94,23 @@ class ClaudeRuntime {
93
94
  });
94
95
  this.#processes.set(turnId, child);
95
96
  child.stdin.end(inputToPrompt(input.input));
97
+ const queue = new AsyncQueue();
96
98
  let buffered = "";
97
99
  let sawMessage = false;
98
100
  const stderr = [];
99
- const events = [];
100
- const push = (event) => {
101
- events.push(event);
101
+ let exitInfo = { code: null, signal: null };
102
+ let spawnError;
103
+ const emit = (event) => {
104
+ queue.push(event);
102
105
  this.#events.emit("event", event);
103
106
  };
107
+ const drainLine = (line) => {
108
+ for (const event of normalizeClaudeLine(line, input.sessionId, turnId, sawMessage)) {
109
+ if (event.type === "message.delta")
110
+ sawMessage = true;
111
+ emit(event);
112
+ }
113
+ };
104
114
  child.stdout.setEncoding("utf8");
105
115
  child.stdout.on("data", (chunk) => {
106
116
  buffered += chunk;
@@ -112,34 +122,57 @@ class ClaudeRuntime {
112
122
  buffered = buffered.slice(newlineIndex + 1);
113
123
  if (!line)
114
124
  continue;
115
- for (const event of normalizeClaudeLine(line, input.sessionId, turnId, sawMessage)) {
116
- if (event.type === "message.delta")
117
- sawMessage = true;
118
- push(event);
119
- }
125
+ drainLine(line);
120
126
  }
121
127
  });
122
128
  child.stderr.setEncoding("utf8");
123
129
  child.stderr.on("data", (chunk) => stderr.push(chunk));
124
- const exit = await waitForExit(child);
125
- this.#processes.delete(turnId);
126
- const trailing = buffered.trim();
127
- if (trailing) {
128
- for (const event of normalizeClaudeLine(trailing, input.sessionId, turnId, sawMessage)) {
129
- if (event.type === "message.delta")
130
- sawMessage = true;
131
- push(event);
130
+ // Delete from #processes only once the OS reports the child is gone
131
+ // (close fires after stdio is drained; error covers spawn failures).
132
+ // Doing this in handlers keeps the map entry reachable for
133
+ // adapter.close() / cancelTurn() during the gap between SIGTERM and
134
+ // actual exit, in case the binary ignores or delays the signal.
135
+ child.on("error", (err) => {
136
+ spawnError = err;
137
+ this.#processes.delete(turnId);
138
+ queue.close();
139
+ });
140
+ child.on("close", (code, signal) => {
141
+ exitInfo = { code, signal };
142
+ const trailing = buffered.trim();
143
+ buffered = "";
144
+ if (trailing)
145
+ drainLine(trailing);
146
+ this.#processes.delete(turnId);
147
+ queue.close();
148
+ });
149
+ try {
150
+ for await (const event of queue)
151
+ yield event;
152
+ if (spawnError) {
153
+ const message = spawnError.message;
154
+ yield { type: "error", sessionId: input.sessionId, turnId, message };
155
+ yield completedTurn(input.sessionId, turnId, "failed", message);
156
+ return;
157
+ }
158
+ if (exitInfo.code === 0) {
159
+ yield completedTurn(input.sessionId, turnId, "completed");
160
+ return;
132
161
  }
162
+ const message = stderr.join("").trim() || `Claude Code exited with code ${exitInfo.code ?? "null"} signal=${exitInfo.signal ?? "null"}`;
163
+ yield { type: "error", sessionId: input.sessionId, turnId, message };
164
+ yield completedTurn(input.sessionId, turnId, "failed", message);
133
165
  }
134
- for (const event of events)
135
- yield event;
136
- if (exit.code === 0) {
137
- yield completedTurn(input.sessionId, turnId, "completed");
138
- return;
166
+ finally {
167
+ // If the consumer aborts the iterator while the child is still
168
+ // alive, send SIGTERM. The close handler above is the single owner
169
+ // of the #processes entry and will delete it once the kernel
170
+ // confirms exit — so callers retain a working handle for graceful
171
+ // shutdown (close()) or escalation (cancelTurn → repeat kill).
172
+ if (child.exitCode === null && child.signalCode === null) {
173
+ child.kill();
174
+ }
139
175
  }
140
- const message = stderr.join("").trim() || `Claude Code exited with code ${exit.code ?? "null"} signal=${exit.signal ?? "null"}`;
141
- yield { type: "error", sessionId: input.sessionId, turnId, message };
142
- yield completedTurn(input.sessionId, turnId, "failed", message);
143
176
  }
144
177
  async #cancelTurn(input) {
145
178
  this.#processes.get(input.turnId)?.kill();
@@ -284,9 +317,6 @@ function textFromClaudeMessage(value) {
284
317
  return text ? [text] : [];
285
318
  });
286
319
  }
287
- function waitForExit(child) {
288
- return new Promise((resolve) => child.on("exit", (code, signal) => resolve({ code, signal })));
289
- }
290
320
  function asRecord(value) {
291
321
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
292
322
  }
@@ -1 +1 @@
1
- {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/providers/codex.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,cAAc,EAEd,aAAa,EAEb,kBAAkB,EAElB,SAAS,EACT,IAAI,EACJ,WAAW,EAGZ,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,KAAK,gBAAgB,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;AAEpD,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,OAAO,CAwCpD,CAAC;AAEF,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAEvF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAIrG;AAiPD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACpC,GAAG,SAAS,CAYZ"}
1
+ {"version":3,"file":"codex.d.ts","sourceRoot":"","sources":["../../src/providers/codex.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGV,cAAc,EAEd,aAAa,EAEb,kBAAkB,EAElB,SAAS,EACT,IAAI,EACJ,WAAW,EAGZ,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAGpD,KAAK,gBAAgB,GAAG,kBAAkB,CAAC,OAAO,CAAC,CAAC;AAEpD,eAAO,MAAM,iBAAiB,EAAE,aAAa,CAAC,OAAO,CAwCpD,CAAC;AAEF,wBAAsB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAEvF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAIrG;AAuUD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACpC,GAAG,SAAS,CAYZ"}
@@ -55,6 +55,13 @@ export async function createCodexAdapter(options) {
55
55
  await runtime.start();
56
56
  return runtime.adapter;
57
57
  }
58
+ /**
59
+ * Cap stderr buffer to a bounded number of lines so a chatty codex
60
+ * process cannot grow memory unbounded over a long-lived session. The
61
+ * tail (most recent N lines) is what we surface in error messages —
62
+ * head lines from a healthy startup are rarely useful for diagnosis.
63
+ */
64
+ const STDERR_BUFFER_MAX_LINES = 200;
58
65
  class CodexRuntime {
59
66
  #options;
60
67
  #process;
@@ -62,6 +69,17 @@ class CodexRuntime {
62
69
  #normalizer = new CodexEventNormalizer();
63
70
  #events = new EventEmitter();
64
71
  #closed = false;
72
+ /**
73
+ * Bounded ring buffer of recent codex stderr lines, surfaced in
74
+ * augmented error messages so a 30ms "JSON-RPC input closed" failure
75
+ * carries the actual codex stderr (auth error, bad flag, missing
76
+ * config, etc.) instead of disappearing into the void.
77
+ */
78
+ #stderrBuffer = [];
79
+ /** Captured at process 'exit' so subsequent rpc.request rejections can cite it. */
80
+ #exitInfo = null;
81
+ /** Captured at process 'error' (e.g. spawn ENOENT) for the same reason. */
82
+ #spawnError = null;
65
83
  constructor(options) {
66
84
  this.#options = options;
67
85
  }
@@ -104,27 +122,84 @@ class CodexRuntime {
104
122
  shell: false,
105
123
  });
106
124
  this.#process.stderr.on("data", (chunk) => {
107
- const message = chunk.toString("utf8").trim();
108
- if (message)
109
- this.#emit({ type: "error", message });
125
+ const text = chunk.toString("utf8");
126
+ // Buffer stderr line-by-line so #augmentError can surface a
127
+ // bounded tail. We still emit the chunk as an error event so
128
+ // existing event-stream consumers aren't broken.
129
+ for (const line of text.split(/\r?\n/)) {
130
+ if (!line)
131
+ continue;
132
+ this.#stderrBuffer.push(line);
133
+ if (this.#stderrBuffer.length > STDERR_BUFFER_MAX_LINES) {
134
+ this.#stderrBuffer.splice(0, this.#stderrBuffer.length - STDERR_BUFFER_MAX_LINES);
135
+ }
136
+ }
137
+ const trimmed = text.trim();
138
+ if (trimmed)
139
+ this.#emit({ type: "error", message: trimmed });
110
140
  });
111
141
  this.#process.on("exit", (code, signal) => {
142
+ this.#exitInfo = { code, signal };
112
143
  if (!this.#closed) {
113
144
  this.#emit({ type: "error", message: `Codex app-server exited: code=${code ?? "null"} signal=${signal ?? "null"}` });
114
145
  }
115
146
  });
147
+ // 'error' fires on spawn failures (e.g. ENOENT when codex binary is
148
+ // missing from PATH). Without a listener Node would crash the
149
+ // process on the unhandled emit; instead capture and let the
150
+ // pending rpc.request rejection surface the spawn error.
151
+ this.#process.on("error", (err) => {
152
+ this.#spawnError = err;
153
+ if (!this.#closed) {
154
+ this.#emit({ type: "error", message: `Codex app-server spawn error: ${err.message}` });
155
+ }
156
+ });
116
157
  this.#rpc = new JsonRpcLineConnection(this.#process.stdout, this.#process.stdin);
117
158
  this.#rpc.onNotification((notification) => {
118
159
  for (const event of this.#normalizer.normalize(notification)) {
119
160
  this.#emit(event);
120
161
  }
121
162
  });
122
- await this.#rpc.request("initialize", {
123
- clientInfo: { name: "glubean-port", version: "0.0.0" },
124
- capabilities: { experimentalApi: true },
125
- });
163
+ try {
164
+ await this.#rpc.request("initialize", {
165
+ clientInfo: { name: "glubean-port", version: "0.0.0" },
166
+ capabilities: { experimentalApi: true },
167
+ });
168
+ }
169
+ catch (err) {
170
+ throw await this.#augmentError(err);
171
+ }
126
172
  this.#rpc.notify("initialized", {});
127
173
  }
174
+ /**
175
+ * Wrap an rpc rejection with codex process diagnostics so callers see
176
+ * (a) why the process closed if it did, (b) any spawn 'error' (e.g.
177
+ * ENOENT), and (c) the most recent stderr lines. The original error
178
+ * is preserved as `cause` for callers that introspect it.
179
+ *
180
+ * Awaits a microtask so any racing `process.on("exit")` fires before
181
+ * we read `#exitInfo` — JsonRpcLineConnection rejects synchronously
182
+ * from `input.on("close")`, but Node may fire stream 'close' before
183
+ * the matching process 'exit' on some versions.
184
+ */
185
+ async #augmentError(err) {
186
+ await new Promise((resolve) => setImmediate(resolve));
187
+ const original = err instanceof Error ? err : new Error(String(err));
188
+ const parts = [original.message];
189
+ if (this.#spawnError) {
190
+ parts.push(`(codex spawn error: ${this.#spawnError.message})`);
191
+ }
192
+ if (this.#exitInfo) {
193
+ parts.push(`(codex process exited: code=${this.#exitInfo.code ?? "null"} signal=${this.#exitInfo.signal ?? "null"})`);
194
+ }
195
+ if (this.#stderrBuffer.length > 0) {
196
+ const tail = this.#stderrBuffer.slice(-20).join("\n");
197
+ parts.push(`(codex stderr tail:\n${tail})`);
198
+ }
199
+ const augmented = new Error(parts.join(" "));
200
+ augmented.cause = original;
201
+ return augmented;
202
+ }
128
203
  async close() {
129
204
  this.#closed = true;
130
205
  this.#rpc?.close();
@@ -152,7 +227,14 @@ class CodexRuntime {
152
227
  });
153
228
  if (method === "thread/resume")
154
229
  params.threadId = input.id ?? "";
155
- const result = asRecord(await rpc.request(method, params));
230
+ let rawResult;
231
+ try {
232
+ rawResult = await rpc.request(method, params);
233
+ }
234
+ catch (err) {
235
+ throw await this.#augmentError(err);
236
+ }
237
+ const result = asRecord(rawResult);
156
238
  const thread = asRecord(result.thread);
157
239
  const id = asString(thread.id) ?? input.id;
158
240
  if (!id)
@@ -183,18 +265,25 @@ class CodexRuntime {
183
265
  const runtimeOptions = resolveRuntimeOptions(input, this.#options.options);
184
266
  const codexOptions = providerRuntimeOptions(runtimeOptions);
185
267
  const effort = toCodexReasoningEffort(codexOptions.effort);
186
- const result = asRecord(await rpc.request("turn/start", {
187
- threadId: input.sessionId,
188
- input: normalizeInput(input.input),
189
- ...(runtimeOptions.cwd ? { cwd: runtimeOptions.cwd } : {}),
190
- ...(runtimeOptions.model ? { model: runtimeOptions.model } : {}),
191
- ...(runtimeOptions.approvalPolicy ? { approvalPolicy: runtimeOptions.approvalPolicy } : {}),
192
- ...(runtimeOptions.sandbox ? { sandboxPolicy: toCodexSandboxPolicy(runtimeOptions.sandbox) } : {}),
193
- ...(effort ? { effort } : {}),
194
- ...(codexOptions?.reasoningSummary ? { summary: codexOptions.reasoningSummary } : {}),
195
- ...(codexOptions?.serviceTier ? { serviceTier: codexOptions.serviceTier } : {}),
196
- ...(input.outputSchema ? { outputSchema: input.outputSchema } : {}),
197
- }));
268
+ let rawResult;
269
+ try {
270
+ rawResult = await rpc.request("turn/start", {
271
+ threadId: input.sessionId,
272
+ input: normalizeInput(input.input),
273
+ ...(runtimeOptions.cwd ? { cwd: runtimeOptions.cwd } : {}),
274
+ ...(runtimeOptions.model ? { model: runtimeOptions.model } : {}),
275
+ ...(runtimeOptions.approvalPolicy ? { approvalPolicy: runtimeOptions.approvalPolicy } : {}),
276
+ ...(runtimeOptions.sandbox ? { sandboxPolicy: toCodexSandboxPolicy(runtimeOptions.sandbox) } : {}),
277
+ ...(effort ? { effort } : {}),
278
+ ...(codexOptions?.reasoningSummary ? { summary: codexOptions.reasoningSummary } : {}),
279
+ ...(codexOptions?.serviceTier ? { serviceTier: codexOptions.serviceTier } : {}),
280
+ ...(input.outputSchema ? { outputSchema: input.outputSchema } : {}),
281
+ });
282
+ }
283
+ catch (err) {
284
+ throw await this.#augmentError(err);
285
+ }
286
+ const result = asRecord(rawResult);
198
287
  expectedTurnId = readTurnId(result);
199
288
  for await (const event of queue) {
200
289
  yield event;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glubean/port",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Neutral runtime adapter for local coding agents.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/node": "^25.6.0",
32
+ "partial-json": "^0.1.7",
32
33
  "typescript": "^6.0.3",
33
34
  "vitest": "^4.1.5"
34
35
  }