@anureo/sdk 0.2.0 → 0.3.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.
Files changed (90) hide show
  1. package/README.md +162 -65
  2. package/package.json +29 -24
  3. package/src/adapter/identity.d.ts +41 -0
  4. package/src/adapter/identity.js +71 -0
  5. package/src/client-options.d.ts +71 -0
  6. package/src/client-options.js +0 -0
  7. package/src/client.d.ts +106 -0
  8. package/src/client.js +254 -0
  9. package/src/diagnostics/errors.d.ts +15 -0
  10. package/src/diagnostics/errors.js +59 -0
  11. package/src/diagnostics/events.d.ts +46 -0
  12. package/src/diagnostics/events.js +47 -0
  13. package/src/diagnostics.d.ts +20 -0
  14. package/src/diagnostics.js +56 -0
  15. package/src/errors.d.ts +48 -0
  16. package/src/errors.js +63 -0
  17. package/src/index.d.ts +27 -0
  18. package/src/index.js +23 -0
  19. package/src/lifecycle/approval.d.ts +16 -0
  20. package/src/lifecycle/approval.js +18 -0
  21. package/src/lifecycle/deferred.d.ts +10 -0
  22. package/src/lifecycle/deferred.js +12 -0
  23. package/src/lifecycle/errors.d.ts +18 -0
  24. package/src/lifecycle/errors.js +45 -0
  25. package/src/lifecycle/events.d.ts +35 -0
  26. package/src/lifecycle/events.js +141 -0
  27. package/src/lifecycle/execution-observation.d.ts +33 -0
  28. package/src/lifecycle/execution-observation.js +0 -0
  29. package/src/lifecycle/immutable.d.ts +12 -0
  30. package/src/lifecycle/immutable.js +28 -0
  31. package/src/lifecycle/input.d.ts +9 -0
  32. package/src/lifecycle/input.js +19 -0
  33. package/src/lifecycle/interactions.d.ts +129 -0
  34. package/src/lifecycle/interactions.js +282 -0
  35. package/src/lifecycle/limits.d.ts +7 -0
  36. package/src/lifecycle/limits.js +13 -0
  37. package/src/lifecycle/result.d.ts +77 -0
  38. package/src/lifecycle/result.js +213 -0
  39. package/src/lifecycle/value.d.ts +4 -0
  40. package/src/lifecycle/value.js +5 -0
  41. package/src/ports.d.ts +224 -0
  42. package/src/ports.js +0 -0
  43. package/src/privacy/file-change-limits.d.ts +20 -0
  44. package/src/privacy/file-change-limits.js +124 -0
  45. package/src/privacy/secrets.d.ts +205 -0
  46. package/src/privacy/secrets.js +564 -0
  47. package/src/process.d.ts +92 -0
  48. package/src/process.js +331 -0
  49. package/src/queue.d.ts +143 -0
  50. package/src/queue.js +266 -0
  51. package/src/request.d.ts +28 -0
  52. package/src/request.js +81 -0
  53. package/src/requests/guard.d.ts +15 -0
  54. package/src/requests/guard.js +93 -0
  55. package/src/session.d.ts +459 -0
  56. package/src/session.js +1135 -0
  57. package/src/structured-output/adapter.d.ts +8 -0
  58. package/src/structured-output/adapter.js +54 -0
  59. package/src/structured-output/bridge.d.ts +81 -0
  60. package/src/structured-output/bridge.js +274 -0
  61. package/src/structured-output/errors.d.ts +17 -0
  62. package/src/structured-output/errors.js +31 -0
  63. package/src/structured-output/limits.d.ts +19 -0
  64. package/src/structured-output/limits.js +0 -0
  65. package/src/structured-output/schema.d.ts +20 -0
  66. package/src/structured-output/schema.js +122 -0
  67. package/src/structured-output/session.d.ts +329 -0
  68. package/src/structured-output/session.js +846 -0
  69. package/src/structured-output/stdio.mjs +83 -0
  70. package/src/structured-output/tool.d.ts +38 -0
  71. package/src/structured-output/tool.js +34 -0
  72. package/src/turn.d.ts +75 -0
  73. package/src/turn.js +95 -0
  74. package/src/types.d.ts +62 -0
  75. package/src/types.js +0 -0
  76. package/dist/diagnostics.d.ts +0 -15
  77. package/dist/diagnostics.js +0 -34
  78. package/dist/errors.d.ts +0 -15
  79. package/dist/file-change-limits.d.ts +0 -5
  80. package/dist/identity.d.ts +0 -13
  81. package/dist/index.d.ts +0 -8
  82. package/dist/index.js +0 -1474
  83. package/dist/interactions.d.ts +0 -20
  84. package/dist/ports.d.ts +0 -66
  85. package/dist/process.d.ts +0 -34
  86. package/dist/process.js +0 -191
  87. package/dist/queue.d.ts +0 -36
  88. package/dist/queue.js +0 -166
  89. package/dist/runtime.d.ts +0 -85
  90. package/dist/secrets.d.ts +0 -28
@@ -0,0 +1,564 @@
1
+ // Registers explicitly marked secrets and projects public events, state and history.
2
+ import { boundFileChangeItem } from "./file-change-limits.js";
3
+ import { assertDefinition } from "@anureo/protocol";
4
+ import { HarnessSdkError } from "../errors.js";
5
+ /**
6
+ * Register explicitly marked answers before projecting content for public observation.
7
+ * This root-exported implementation helper is owned by Session, which installs registration in
8
+ * InteractionRegistry's pre-response callback and projects events and request results. Direct
9
+ * consumers own the same registration/publication order. Secrets and pending prefixes stay in
10
+ * memory; the optional synchronous hook owns persistence. No native resources are acquired.
11
+ */
12
+ export class SecretRedactor {
13
+ onRemember;
14
+ /**
15
+ * Create an empty in-memory registry with an optional synchronous persistence hook. Session
16
+ * forwards runtime.onMarkedSecret here; rememberScalar invokes it before accepting each new value.
17
+ * Construction only retains the hook. The host owns persistence and any effects its hook performs.
18
+ */
19
+ constructor(onRemember) {
20
+ this.onRemember = onRemember;
21
+ }
22
+ /**
23
+ * Register answers marked secret/writeOnly by the request before exposing or delivering them.
24
+ * Marked containers contribute their scalar descendants. Numbers and booleans register by string
25
+ * value, while empty strings, null and duplicates are ignored. At most 128 distinct values and
26
+ * 64 KiB of UTF-8 text are retained. Exceeding either cap throws a HarnessSdkError with code
27
+ * BACKPRESSURE_EXCEEDED. Hook exceptions also propagate, leaving earlier registrations intact.
28
+ * Schema traversal stops beyond depth 32, with the request schema at depth zero.
29
+ * Session installs this as InteractionRegistry's pre-response check. InteractionRegistry.respond
30
+ * invokes it after validation and rejects on failure before looking up a replay or starting native
31
+ * delivery. Direct consumers own validation and delivery.
32
+ */
33
+ rememberResponse(request, response) {
34
+ if (request.kind === "userInput" && response.kind === "userInput") {
35
+ for (const question of request.questions) {
36
+ if (question.secret)
37
+ this.remember(response.answers[question.id]);
38
+ }
39
+ }
40
+ if (request.kind === "elicitation" && response.kind === "elicitation" && response.content && request.schema !== undefined) {
41
+ this.fromSchema(request.schema, response.content, { root: request.schema, depth: 0 });
42
+ }
43
+ if (request.kind === "dynamicTool" && response.kind === "dynamicTool" && request.schema !== undefined) {
44
+ this.fromSchema(request.schema, response, { root: request.schema, depth: 0 });
45
+ }
46
+ }
47
+ /**
48
+ * Replace complete secret matches with [redacted], preferring the longest match at each position.
49
+ * Unmatched trailing prefixes are emitted without retaining stream state. Session.projectOutput
50
+ * and Session.projectSubmissionStatus use this before exposure. OutputSession.publishOutput
51
+ * revalidates JSON returned through projectOutput and converts schema failures into failed turns.
52
+ */
53
+ text(value) { return this.values.size ? this.scan(value, "complete").text : value; }
54
+ /**
55
+ * Project an ordered text chunk, withholding a possible trailing secret prefix until the next chunk.
56
+ * Complete matches win even when a longer secret is incomplete. event scopes streams by turn/item
57
+ * and discards pending prefixes on item/completed, whose snapshot carries authoritative full text.
58
+ * Direct consumers must use distinct ordered keys; this method has no flush/reset operation, so
59
+ * they must discard the redactor when they no longer need its retained prefixes and secrets.
60
+ */
61
+ delta(stream, value) {
62
+ if (!this.values.size)
63
+ return value;
64
+ const result = this.scan((this.streams.get(stream) ?? "") + value, "stream");
65
+ if (result.pending)
66
+ this.streams.set(stream, result.pending);
67
+ else
68
+ this.streams.delete(stream);
69
+ return result.text;
70
+ }
71
+ /**
72
+ * Return recursively projected JSON without mutating the input. Marked numbers and booleans become
73
+ * the string [redacted]. If projected keys collide, the last entry in traversal order wins.
74
+ * Session.projectSubmissionStatus selects receipt and error content. Direct callers should also
75
+ * pass only content fields to preserve protocol identity.
76
+ */
77
+ json(value) {
78
+ if (typeof value === "string")
79
+ return this.text(value);
80
+ if (typeof value === "number" || typeof value === "boolean")
81
+ return this.values.has(String(value)) ? "[redacted]" : value;
82
+ if (Array.isArray(value))
83
+ return value.map((entry) => this.json(entry));
84
+ if (value && typeof value === "object")
85
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [this.text(key), this.json(child)]));
86
+ return value;
87
+ }
88
+ /**
89
+ * Project configuration annotations and values without modifying validation constraints or
90
+ * property names. Return the original value when no configuration is advertised. state uses this
91
+ * for session snapshots; the caller owns storing or publishing the result.
92
+ */
93
+ capabilities(value) {
94
+ if (!value.config)
95
+ return value;
96
+ return {
97
+ ...value,
98
+ config: {
99
+ ...value.config,
100
+ options: value.config.options.map((option) => ({
101
+ ...option,
102
+ ...(option.title !== undefined ? { title: this.text(option.title) } : {}),
103
+ ...(option.value !== undefined ? { value: this.json(option.value) } : {}),
104
+ schema: this.schema(option.schema),
105
+ })),
106
+ },
107
+ };
108
+ }
109
+ /**
110
+ * Return a new outer value with projected goal JSON and unchanged native goal metadata.
111
+ * state and result call this before exposing goal content; no session state is written here.
112
+ */
113
+ goal(value) { return { ...value, goal: value.goal === null ? null : this.json(value.goal) }; }
114
+ /**
115
+ * Project content-bearing state fields while preserving bindings, protocol enums and revisions; no
116
+ * session bookkeeping is changed. Session.snapshot calls this on reads; event and result use it
117
+ * for state/config observations. Session freezes reads and validates/stores request results;
118
+ * direct consumers retain responsibility for validation and publication.
119
+ */
120
+ state(value) {
121
+ return {
122
+ ...value,
123
+ ...(value.metadata ? { metadata: this.json(value.metadata) } : {}),
124
+ config: Object.fromEntries(Object.entries(value.config).map(([id, entry]) => [id, this.json(entry)])),
125
+ capabilities: this.capabilities(value.capabilities),
126
+ ...(value.goal ? { goal: this.goal(value.goal) } : {}),
127
+ };
128
+ }
129
+ /**
130
+ * Project a validated configuration or goal result according to its method. Other results pass
131
+ * through by identity. Session.projectRequestResult routes history to history() and calls this
132
+ * for other projectable results. Session.executeRequest stores projected configuration or goal
133
+ * state and validates and freezes the returned result. Direct callers own validation and storage.
134
+ */
135
+ result(method, value) {
136
+ if (method === "session/config/set")
137
+ return this.state(value);
138
+ if (method === "goal/set" || method === "goal/clear")
139
+ return this.goal(value);
140
+ return value;
141
+ }
142
+ /**
143
+ * Project validated item content without changing protocol identity or registering new secrets.
144
+ * Tool-call file changes additionally use the shared material/mirror budget. event uses this for
145
+ * snapshots and validated patches; history applies it to each native entry. Direct consumers
146
+ * must validate item shape first, as Session and deltaValue do for their respective input paths.
147
+ */
148
+ item(item) {
149
+ if (item.extensions)
150
+ item = { ...item, extensions: this.json(item.extensions) };
151
+ switch (item.type) {
152
+ case "agentMessage": return { ...item, text: this.text(item.text) };
153
+ case "userMessage": return { ...item, content: item.content.map((block) => this.input(block)) };
154
+ case "reasoning": return {
155
+ ...item,
156
+ ...(item.text ? { text: this.text(item.text) } : {}),
157
+ ...(item.summary ? { summary: this.text(item.summary) } : {}),
158
+ };
159
+ case "toolCall": return this.toolCall(item);
160
+ case "plan": return { ...item, steps: item.steps.map((step) => ({ ...step, text: this.text(step.text) })) };
161
+ case "subagent": return {
162
+ ...item,
163
+ ...(item.name ? { name: this.text(item.name) } : {}),
164
+ ...(item.task ? { task: this.text(item.task) } : {}),
165
+ };
166
+ case "review": return { ...item, ...(item.text ? { text: this.text(item.text) } : {}) };
167
+ case "compaction": return { ...item, ...(item.summary ? { summary: this.text(item.summary) } : {}) };
168
+ case "hook": return {
169
+ ...item,
170
+ name: this.text(item.name),
171
+ ...(item.input !== undefined ? { input: this.json(item.input) } : {}),
172
+ ...(item.output !== undefined ? { output: this.json(item.output) } : {}),
173
+ };
174
+ case "notice": return { ...item, text: this.text(item.text) };
175
+ }
176
+ }
177
+ /**
178
+ * Project public request content while preserving identifiers, answer keys and protocol alternatives.
179
+ * event calls this after Session.requestInteraction applies protocol publicInteraction; direct
180
+ * consumers must likewise apply protocol privacy rules first. If a privatePayload is supplied
181
+ * directly, this method projects its JSON content and retains the field. InteractionRegistry
182
+ * retains the native request for response validation and marked-field lookup.
183
+ */
184
+ interaction(request) {
185
+ const common = {
186
+ ...request,
187
+ ...(request.title ? { title: this.text(request.title) } : {}),
188
+ ...(request.privatePayload ? { privatePayload: this.json(request.privatePayload) } : {}),
189
+ };
190
+ switch (common.kind) {
191
+ case "approval": return {
192
+ ...common,
193
+ ...(common.input !== undefined ? { input: this.json(common.input) } : {}),
194
+ options: common.options.map((option) => ({
195
+ ...option,
196
+ label: this.text(option.label),
197
+ ...(option.description ? { description: this.text(option.description) } : {}),
198
+ })),
199
+ };
200
+ case "userInput": return {
201
+ ...common,
202
+ questions: common.questions.map((question) => ({
203
+ ...question,
204
+ prompt: this.text(question.prompt),
205
+ ...(question.options ? { options: question.options.map((option) => ({ ...option, label: this.text(option.label) })) } : {}),
206
+ })),
207
+ };
208
+ case "elicitation": return { ...common, ...(common.message ? { message: this.text(common.message) } : {}) };
209
+ case "dynamicTool": return { ...common, arguments: this.json(common.arguments) };
210
+ }
211
+ }
212
+ /**
213
+ * Project validated events in publication order, clearing an item's buffered prefix on completion.
214
+ * Supply turnId to isolate reused item IDs across turns. Omission uses an item-only stream key.
215
+ * Session.publish invokes this after lifecycle admission and before retaining/publishing results.
216
+ * An invalid Item-shaped patch throws ProtocolValidationError and rejects Session.emit, which
217
+ * starts observation teardown. Direct consumers own event validation, ordering and publication.
218
+ * Completed snapshots supply authoritative item content.
219
+ */
220
+ event(event, turnId) {
221
+ switch (event.type) {
222
+ case "item/started":
223
+ case "item/completed": {
224
+ if (event.type === "item/completed")
225
+ this.streams.delete(streamKey(event.payload.item.id, turnId));
226
+ return { ...event, payload: { item: this.item(event.payload.item) } };
227
+ }
228
+ case "item/delta": return { ...event, payload: this.itemDelta(event.payload, turnId) };
229
+ case "notice": return {
230
+ ...event,
231
+ payload: {
232
+ ...event.payload,
233
+ message: this.text(event.payload.message),
234
+ ...(event.payload.raw !== undefined ? { raw: this.json(event.payload.raw) } : {}),
235
+ },
236
+ };
237
+ case "extension": return { ...event, payload: { ...event.payload, data: this.json(event.payload.data) } };
238
+ case "session/metadata": return { ...event, payload: this.json(event.payload) };
239
+ case "session/state": return { ...event, payload: this.state(event.payload) };
240
+ case "interaction/requested": return { ...event, payload: this.interaction(event.payload) };
241
+ case "turn/completed": return {
242
+ ...event,
243
+ payload: {
244
+ ...event.payload,
245
+ ...(event.payload.error ? {
246
+ error: {
247
+ ...event.payload.error,
248
+ message: this.text(event.payload.error.message),
249
+ ...(event.payload.error.details ? { details: this.json(event.payload.error.details) } : {}),
250
+ },
251
+ } : {}),
252
+ },
253
+ };
254
+ default: return event;
255
+ }
256
+ }
257
+ /**
258
+ * Project a prevalidated history page using live items' privacy/retention rules, preserving
259
+ * pagination and execution identities. The input must already be validated; no history is fetched
260
+ * here. Session.projectRequestResult calls this before executeRequest awaits the optional host
261
+ * redactHistory hook, validates and freezes the result; host-hook rejection fails that request.
262
+ */
263
+ history(page) { return { ...page, items: page.items.map((entry) => ({ ...entry, item: this.item(entry.item) })) }; }
264
+ values = new Set();
265
+ streams = new Map();
266
+ secretBytes = 0;
267
+ /**
268
+ * rememberResponse and fromSchema call this before a response can be sent. Recursively retain marked
269
+ * scalar values within session budgets; invoke the host hook before accepting each new value.
270
+ * Capacity or hook failures throw to InteractionRegistry.respond before native delivery; values
271
+ * already accepted during the traversal remain registered, with no rollback of host effects.
272
+ */
273
+ remember(value) {
274
+ if (value === undefined || value === null)
275
+ return;
276
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
277
+ this.rememberScalar(value);
278
+ return;
279
+ }
280
+ if (Array.isArray(value)) {
281
+ value.forEach((entry) => this.remember(entry));
282
+ return;
283
+ }
284
+ for (const entry of Object.values(value))
285
+ this.remember(entry);
286
+ }
287
+ /**
288
+ * Accept a new nonempty scalar only after capacity checks and the synchronous persistence hook
289
+ * succeed. remember preserves traversal order, so a later failure never rolls back earlier values;
290
+ * a failed hook leaves this value unregistered and retryable.
291
+ */
292
+ rememberScalar(value) {
293
+ const text = String(value);
294
+ if (!text || this.values.has(text))
295
+ return;
296
+ const bytes = new TextEncoder().encode(text).byteLength;
297
+ if (this.values.size >= 128 || this.secretBytes + bytes > 64 * 1024) {
298
+ throw new HarnessSdkError("BACKPRESSURE_EXCEEDED", "Marked secret limit exceeded");
299
+ }
300
+ this.onRemember?.(text);
301
+ this.values.add(text);
302
+ this.secretBytes += bytes;
303
+ }
304
+ /**
305
+ * Resolve a reference beginning with #/ against the original request schema. Decode JSON Pointer
306
+ * escapes and follow object keys. A missing key or non-object intermediate yields undefined.
307
+ * fromSchema checks the returned node's shape before visiting it.
308
+ */
309
+ resolveLocalReference(reference, root) {
310
+ let target = root;
311
+ for (const token of reference.slice(2).split("/")) {
312
+ const key = token.replace(/~1/g, "/").replace(/~0/g, "~");
313
+ target = isJsonObject(target) ? target[key] : undefined;
314
+ }
315
+ return target;
316
+ }
317
+ /**
318
+ * rememberResponse calls this for elicitation/dynamicTool schemas; recursive branches pass marked
319
+ * values to remember. Collect marked values through bounded local schema traversal, including
320
+ * referenced and sibling rules; does not validate response values or fetch references. Visit
321
+ * references before siblings because registration order determines host-hook and budget failures.
322
+ * Stop beyond depth 32; registration failures propagate without undoing earlier registrations.
323
+ */
324
+ fromSchema(schema, value, { root, depth }) {
325
+ if (depth > 32 || typeof schema !== "object" || !schema || value === undefined)
326
+ return;
327
+ if (schema.writeOnly === true || schema.secret === true) {
328
+ this.remember(value);
329
+ return;
330
+ }
331
+ const children = { root, depth: depth + 1 };
332
+ if (typeof schema.$ref === "string" && schema.$ref.startsWith("#/") && typeof root === "object") {
333
+ const target = this.resolveLocalReference(schema.$ref, root);
334
+ if (isJsonSchema(target)) {
335
+ this.fromSchema(target, value, children);
336
+ }
337
+ }
338
+ if (isJsonObject(value))
339
+ this.rememberObjectProperties(schema, value, children);
340
+ if (Array.isArray(value))
341
+ this.rememberArrayItems(schema, value, children);
342
+ this.rememberComposedSchemas(schema, value, children);
343
+ }
344
+ /**
345
+ * fromSchema calls this between reference and array/composition visits. Register declared
346
+ * properties before additional ones, sharing the next-depth context and propagating registration
347
+ * failures. Even a declared non-schema property excludes that key from additionalProperties.
348
+ */
349
+ rememberObjectProperties(schema, value, context) {
350
+ const properties = schema.properties;
351
+ if (isJsonObject(properties)) {
352
+ for (const [key, child] of Object.entries(properties)) {
353
+ if (isJsonSchema(child))
354
+ this.fromSchema(child, value[key], context);
355
+ }
356
+ }
357
+ if (isJsonObject(schema.additionalProperties)) {
358
+ for (const [key, child] of Object.entries(value)) {
359
+ if (!properties || typeof properties !== "object" || !Object.hasOwn(properties, key)) {
360
+ this.fromSchema(schema.additionalProperties, child, context);
361
+ }
362
+ }
363
+ }
364
+ }
365
+ /**
366
+ * fromSchema visits array elements in order under the same items schema and next-depth context.
367
+ * Tuple arrays and absent items rules contribute no registrations. Registration failures propagate.
368
+ */
369
+ rememberArrayItems(schema, value, context) {
370
+ if (!schema.items || !isJsonSchema(schema.items))
371
+ return;
372
+ for (const child of value) {
373
+ this.fromSchema(schema.items, child, context);
374
+ }
375
+ }
376
+ /**
377
+ * fromSchema visits composition branches after reference, object and array rules. Visit every
378
+ * schema branch in keyword and array order without selecting a validation match. Registration
379
+ * failures stop the remaining visits.
380
+ */
381
+ rememberComposedSchemas(schema, value, context) {
382
+ for (const key of ["allOf", "anyOf", "oneOf"]) {
383
+ const branches = schema[key];
384
+ if (!Array.isArray(branches))
385
+ continue;
386
+ for (const branch of branches) {
387
+ if (isJsonSchema(branch))
388
+ this.fromSchema(branch, value, context);
389
+ }
390
+ }
391
+ }
392
+ /**
393
+ * text consumes the complete scan; delta stores a stream scan's pending prefix for the next chunk.
394
+ * Return longest complete matches as redactions. Stream mode withholds an unmatched trailing
395
+ * prefix so it cannot leak before the next chunk arrives; complete mode emits that prefix.
396
+ * This scan reads registered secrets but changes no stream or registration state.
397
+ */
398
+ scan(value, mode) {
399
+ const secretsByDescendingLength = [...this.values].sort((a, b) => b.length - a.length);
400
+ const output = [];
401
+ for (let offset = 0; offset < value.length;) {
402
+ const candidates = secretsByDescendingLength.filter((secret) => secret[0] === value[offset]);
403
+ // Prefer the longest complete match, even if a longer secret is still incomplete.
404
+ const completeMatch = candidates.find((entry) => value.startsWith(entry, offset));
405
+ if (completeMatch) {
406
+ output.push("[redacted]");
407
+ offset += completeMatch.length;
408
+ continue;
409
+ }
410
+ if (mode === "stream" && candidates.length) {
411
+ const suffix = value.slice(offset);
412
+ if (candidates.some((entry) => entry.startsWith(suffix))) {
413
+ return { text: output.join(""), pending: suffix };
414
+ }
415
+ }
416
+ output.push(value.charAt(offset));
417
+ offset++;
418
+ }
419
+ return { text: output.join(""), pending: "" };
420
+ }
421
+ /**
422
+ * capabilities calls this for config-option schemas before exposing state. Project schema
423
+ * annotations/defaults recursively while preserving constraints and property names; privacy must not
424
+ * silently change validation rules. Object schemas are copied and boolean schemas pass through.
425
+ */
426
+ schema(schema) {
427
+ if (typeof schema === "boolean")
428
+ return schema;
429
+ const output = { ...schema };
430
+ for (const key of ["title", "description", "default", "examples"])
431
+ if (output[key] !== undefined)
432
+ output[key] = this.json(output[key]);
433
+ for (const key of ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"]) {
434
+ const map = output[key];
435
+ if (isJsonObject(map))
436
+ output[key] = Object.fromEntries(Object.entries(map).map(([name, child]) => [name, isJsonSchema(child) ? this.schema(child) : child]));
437
+ }
438
+ for (const key of ["additionalProperties", "unevaluatedProperties", "propertyNames", "items", "contains", "additionalItems", "unevaluatedItems", "not", "if", "then", "else", "contentSchema"]) {
439
+ const child = output[key];
440
+ if (isJsonSchema(child))
441
+ output[key] = this.schema(child);
442
+ }
443
+ for (const key of ["allOf", "anyOf", "oneOf", "prefixItems"]) {
444
+ const children = output[key];
445
+ if (Array.isArray(children))
446
+ output[key] = children.map((child) => isJsonSchema(child) ? this.schema(child) : child);
447
+ }
448
+ return output;
449
+ }
450
+ /**
451
+ * item maps user-message content through this projection. Project content-bearing input block fields
452
+ * while preserving modality and protocol structure; return a detached outer block without loading
453
+ * files or fetching URLs. item returns the block in the public user message.
454
+ */
455
+ input(block) {
456
+ if (block.type === "text")
457
+ return { ...block, text: this.text(block.text) };
458
+ if (block.type === "file")
459
+ return { ...block, uri: this.text(block.uri), ...(block.name ? { name: this.text(block.name) } : {}) };
460
+ return { ...block, ...(block.url ? { url: this.text(block.url) } : {}), ...(block.data ? { data: this.text(block.data) } : {}) };
461
+ }
462
+ /**
463
+ * toolCall merges these fields into projected tool-call items before bounding file-change material.
464
+ * Project tool image paths/prompts only; image identity and other native metadata remain
465
+ * authoritative. Return only the image field (or no fields) without reading or modifying an image.
466
+ */
467
+ toolImage(item) {
468
+ return item.image ? {
469
+ image: {
470
+ ...item.image,
471
+ ...(item.image.path ? { path: this.text(item.image.path) } : {}),
472
+ ...(item.image.revisedPrompt ? { revisedPrompt: this.text(item.image.revisedPrompt) } : {}),
473
+ },
474
+ } : {};
475
+ }
476
+ /**
477
+ * toolCall maps fileChanges through this before boundFileChangeItem enforces retention.
478
+ * Project change paths and material text while preserving source metadata; the caller subsequently
479
+ * applies the shared retention budget. Return copied text material without applying patches or
480
+ * changing unavailable-material metadata.
481
+ */
482
+ fileChange(change) {
483
+ return {
484
+ ...change,
485
+ path: this.text(change.path),
486
+ ...(change.oldPath !== undefined ? { oldPath: this.text(change.oldPath) } : {}),
487
+ materials: change.materials.map((material) => {
488
+ if (material.format === "unified")
489
+ return { ...material, patch: this.text(material.patch) };
490
+ if (material.format === "text")
491
+ return {
492
+ ...material,
493
+ oldText: material.oldText === null ? null : this.text(material.oldText),
494
+ newText: this.text(material.newText),
495
+ };
496
+ return material;
497
+ }),
498
+ };
499
+ }
500
+ /**
501
+ * item delegates tool content projection here after extensions have been projected. Redact every
502
+ * content field before enforcing the shared file-change budget so accounting measures projected
503
+ * text. Return the bounded snapshot without changing native identity or source material.
504
+ */
505
+ toolCall(item) {
506
+ const redactedItem = {
507
+ ...item,
508
+ ...this.toolImage(item),
509
+ ...(item.fileChanges !== undefined ? { fileChanges: item.fileChanges.map((change) => this.fileChange(change)) } : {}),
510
+ title: this.text(item.title),
511
+ ...(item.input !== undefined ? { input: this.json(item.input) } : {}),
512
+ ...(item.output !== undefined ? { output: this.json(item.output) } : {}),
513
+ ...(item.raw !== undefined ? { raw: this.json(item.raw) } : {}),
514
+ ...(item.locations ? { locations: item.locations.map((location) => ({ ...location, path: this.text(location.path) })) } : {}),
515
+ };
516
+ return boundFileChangeItem(redactedItem);
517
+ }
518
+ /**
519
+ * itemDelta calls this for JSON-valued payloads before Session retains or publishes the event.
520
+ * A patch object with type or fileChanges must pass Item validation before item projection.
521
+ * Item-shaped validation failures throw ProtocolValidationError. Other values receive ordinary
522
+ * JSON projection.
523
+ */
524
+ deltaValue(kind, value) {
525
+ if (kind === "patch" && isJsonObject(value) && ("type" in value || "fileChanges" in value)) {
526
+ assertDefinition("Item", value);
527
+ return this.item(value);
528
+ }
529
+ return this.json(value);
530
+ }
531
+ /**
532
+ * event projects delta text before validating/projecting an optional JSON value. A malformed patch
533
+ * throws after any prefix update, as publication fails through Session.emit; no buffered state is
534
+ * rolled back. Non-Item JSON still receives ordinary content projection.
535
+ */
536
+ itemDelta(payload, turnId) {
537
+ const projected = { ...payload };
538
+ if (payload.text !== undefined) {
539
+ projected.text = this.delta(streamKey(payload.itemId, turnId), payload.text);
540
+ }
541
+ if (payload.value !== undefined) {
542
+ projected.value = this.deltaValue(payload.kind, payload.value);
543
+ }
544
+ return projected;
545
+ }
546
+ }
547
+ /**
548
+ * Schema collection, projection and local-reference resolution use this pure shape guard before
549
+ * reading named children. Arrays and null cannot supply schema property maps.
550
+ */
551
+ function isJsonObject(value) {
552
+ return !!value && typeof value === "object" && !Array.isArray(value);
553
+ }
554
+ /**
555
+ * fromSchema, rememberObjectProperties and schema use this pure guard for selected schema children.
556
+ * Accept boolean/object nodes without validating schema keywords or response values.
557
+ */
558
+ function isJsonSchema(value) {
559
+ return typeof value === "boolean" || isJsonObject(value);
560
+ }
561
+ /** Share turn/item key encoding between delta buffering and completion cleanup; omission keeps the legacy item-only key. */
562
+ function streamKey(itemId, turnId) {
563
+ return turnId === undefined ? itemId : JSON.stringify([turnId, itemId]);
564
+ }
@@ -0,0 +1,92 @@
1
+ /** Child exit status observed independently of output EOF. */
2
+ export type ProcessExit = {
3
+ /** Exit status, or null for signal termination or startup failure. */
4
+ code: number | null;
5
+ /** Terminating signal, or null for normal exit or startup failure. */
6
+ signal: string | null;
7
+ /** Native code, if supplied, when the exit observer receives a child error without a PID. */
8
+ errorCode?: string;
9
+ };
10
+ /** Configure a shell-free harness launch and choose who is responsible for terminating the child. */
11
+ export interface HarnessProcessOptions {
12
+ /** Executable path or name. Empty strings and NUL bytes are rejected before launch. */
13
+ command: string;
14
+ /** Literal argument vector, empty by default. Entries containing NUL bytes are rejected before launch. */
15
+ args?: readonly string[];
16
+ /** Working directory for the child; omitted means the parent working directory. */
17
+ cwd?: string;
18
+ /** Complete environment for the child. Omit to inherit the parent environment. */
19
+ env?: Record<string, string | undefined>;
20
+ /** Defaults to true. Set to false when another owner will terminate the launched child. */
21
+ owned?: boolean;
22
+ /**
23
+ * Grace period before escalation for an owned child. Accepts integer milliseconds from 0 to
24
+ * 60,000 and defaults to 1000. Validated for either ownership mode.
25
+ */
26
+ shutdownTimeoutMs?: number;
27
+ }
28
+ /**
29
+ * Local I/O handles for one harness process. Drain output and await disposal even after exit.
30
+ * Custom process factories for the AHP, Codex, and Hermes adapters also supply this handle.
31
+ * The adapter receiving it takes responsibility for disposal.
32
+ */
33
+ export interface HarnessProcess {
34
+ /** PID captured at launch and retained after exit. */
35
+ readonly pid: number;
36
+ /** True when this handle is responsible for terminating the process tree on disposal. */
37
+ readonly owned: boolean;
38
+ /** Child stdout bytes; callers should drain buffered output before disposal. */
39
+ readonly stdout: AsyncIterable<Uint8Array>;
40
+ /** Child stderr bytes; callers should drain buffered output before disposal. */
41
+ readonly stderr: AsyncIterable<Uint8Array>;
42
+ /**
43
+ * Resolves with status on child exit or an observed child error without a PID. Read the output
44
+ * streams through EOF to collect buffered bytes.
45
+ */
46
+ readonly exited: Promise<ProcessExit>;
47
+ /**
48
+ * Write once to stdin and resolve when its native write callback completes, without waiting for
49
+ * the peer to consume the bytes. Reject with PROCESS_WRITE_FAILED if the input is closed, exit
50
+ * has been observed, disposal has started, or the native write callback reports an error.
51
+ */
52
+ write(chunk: string | Uint8Array): Promise<void>;
53
+ /**
54
+ * End stdin after queued writes. Await writes first to observe each write's outcome. Resolve when
55
+ * the native end callback completes, or immediately if input is already ending or destroyed.
56
+ * A native close callback error rejects with PROCESS_WRITE_FAILED. Output and exit remain observable.
57
+ */
58
+ closeInput(): Promise<void>;
59
+ /**
60
+ * Release local stdin, stdout, and stderr even when termination fails. Drain buffered output first.
61
+ * For an owned child, terminate the tree, escalating as needed, then allow up to 1000 ms for any
62
+ * pending child exit observation. Termination failure or a missing exit observation rejects with
63
+ * PROCESS_DISPOSE_FAILED. For an external child, unref it and release the pipes without waiting for
64
+ * exit. Every call returns the same promise, including after rejection.
65
+ */
66
+ dispose(): Promise<void>;
67
+ }
68
+ /** Process failure category for adapters; native causes are reduced to a format-checked code. */
69
+ export declare class ProcessPortError extends Error {
70
+ /**
71
+ * Construct a failure using caller-supplied safe message text, which is retained verbatim.
72
+ * Supply a native cause to retain only its format-checked error code.
73
+ */
74
+ constructor(code: ProcessPortError['code'], message: string, options?: ErrorOptions);
75
+ /** Stable category for adapter startup, input, or disposal failures. */
76
+ readonly code: 'HARNESS_UNAVAILABLE' | 'PROCESS_WRITE_FAILED' | 'PROCESS_DISPOSE_FAILED';
77
+ /**
78
+ * Native code of 1 to 64 characters, beginning with an uppercase letter and containing only
79
+ * uppercase letters, digits, and underscores.
80
+ */
81
+ readonly nativeCode?: string;
82
+ }
83
+ /**
84
+ * Launch a harness executable directly with piped I/O and literal arguments, without a shell.
85
+ * Resolve after the spawn event provides a process handle. The caller handles protocol readiness.
86
+ * Invalid inputs or launch failures reject with HARNESS_UNAVAILABLE. Failed startup releases any
87
+ * local pipes it acquired. Drain output and await disposal even after child exit.
88
+ *
89
+ * ACP's processStream awaits launch to wire the pipes into its channel, propagates launch
90
+ * rejection, and delegates channel disposal to the handle.
91
+ */
92
+ export declare function spawnHarnessProcess(options: HarnessProcessOptions): Promise<HarnessProcess>;