@noetic-tools/core 5.3.0 → 5.4.1

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 (94) hide show
  1. package/README.md +8 -6
  2. package/dist/adapters/in-memory-subprocess/step-completion.js +1 -1
  3. package/dist/adapters/openrouter.d.ts +12 -1
  4. package/dist/adapters/openrouter.js +79 -5
  5. package/dist/builders/acp-agent-tool.d.ts +67 -0
  6. package/dist/builders/acp-agent-tool.js +87 -0
  7. package/dist/builders/control-flow-builders.d.ts +13 -13
  8. package/dist/builders/control-flow-builders.js +11 -11
  9. package/dist/{patterns → builders}/dynamic-workflow.d.ts +1 -1
  10. package/dist/{patterns → builders}/dynamic-workflow.js +20 -14
  11. package/dist/builders/every.d.ts +14 -14
  12. package/dist/builders/every.js +16 -16
  13. package/dist/builders/layer-provides-builders.d.ts +1 -1
  14. package/dist/builders/layer-provides-builders.js +1 -1
  15. package/dist/builders/provide-builder.d.ts +5 -12
  16. package/dist/builders/provide-builder.js +9 -10
  17. package/dist/builders/spawn-builder.d.ts +0 -2
  18. package/dist/builders/spawn-builder.js +2 -3
  19. package/dist/builders/step-builders.d.ts +103 -92
  20. package/dist/builders/step-builders.js +146 -163
  21. package/dist/builders/tool-builder.d.ts +18 -22
  22. package/dist/builders/tool-builder.js +6 -4
  23. package/dist/builders/workflow-hydrator.d.ts +11 -7
  24. package/dist/builders/workflow-hydrator.js +75 -87
  25. package/dist/builders/workflow-step.d.ts +15 -27
  26. package/dist/builders/workflow-step.js +14 -19
  27. package/dist/conditions/conditions.d.ts +2 -2
  28. package/dist/conditions/conditions.js +1 -1
  29. package/dist/harness/agent-harness.d.ts +89 -46
  30. package/dist/harness/agent-harness.js +93 -39
  31. package/dist/harness/deps/types.d.ts +1 -1
  32. package/dist/harness/model-call.d.ts +1 -1
  33. package/dist/harness/model-call.js +23 -18
  34. package/dist/index.d.ts +27 -35
  35. package/dist/index.js +13 -29
  36. package/dist/interpreter/acp-events.d.ts +67 -0
  37. package/dist/interpreter/acp-events.js +256 -0
  38. package/dist/interpreter/acp-history.d.ts +22 -0
  39. package/dist/interpreter/acp-history.js +58 -0
  40. package/dist/interpreter/action-types.d.ts +1 -1
  41. package/dist/interpreter/collect-tools.js +10 -13
  42. package/dist/interpreter/execute-acp-agent.d.ts +10 -0
  43. package/dist/interpreter/execute-acp-agent.js +505 -0
  44. package/dist/interpreter/execute-action.d.ts +5 -5
  45. package/dist/interpreter/execute-action.js +284 -218
  46. package/dist/interpreter/execute-control.d.ts +9 -9
  47. package/dist/interpreter/execute-control.js +27 -27
  48. package/dist/interpreter/execute.d.ts +1 -1
  49. package/dist/interpreter/execute.js +29 -35
  50. package/dist/interpreter/step-dispatcher.d.ts +1 -1
  51. package/dist/interpreter/step-dispatcher.js +3 -3
  52. package/dist/interpreter/structured-output.d.ts +14 -0
  53. package/dist/interpreter/structured-output.js +40 -0
  54. package/dist/interpreter/typeguards.d.ts +1 -1
  55. package/dist/interpreter/typeguards.js +1 -1
  56. package/dist/portable/builders.d.ts +5 -3
  57. package/dist/portable/builders.js +8 -5
  58. package/dist/portable/context-layers.d.ts +1 -1
  59. package/dist/portable/context-layers.js +1 -1
  60. package/dist/runtime/acp-session-store.d.ts +75 -0
  61. package/dist/runtime/acp-session-store.js +0 -0
  62. package/dist/runtime/broadcaster-utils.d.ts +1 -1
  63. package/dist/runtime/broadcaster-utils.js +1 -1
  64. package/dist/runtime/context-impl.d.ts +8 -10
  65. package/dist/runtime/context-impl.js +8 -12
  66. package/dist/runtime/durable/step-ledger.d.ts +2 -2
  67. package/dist/runtime/durable/step-ledger.js +3 -3
  68. package/dist/runtime/session-runner.d.ts +12 -1
  69. package/dist/runtime/session-runner.js +33 -1
  70. package/dist/runtime/tool-context.js +0 -2
  71. package/dist/schemas/workflow.d.ts +3 -3
  72. package/dist/schemas/workflow.js +2 -2
  73. package/dist/types/checkpoint.d.ts +11 -2
  74. package/dist/types/checkpoint.js +10 -1
  75. package/dist/{runtime → util}/detached-handle.js +1 -1
  76. package/package.json +6 -4
  77. package/schema/noetic-workflow.schema.json +239 -260
  78. package/dist/builders/context-option.d.ts +0 -23
  79. package/dist/builders/context-option.js +0 -22
  80. package/dist/deprecated.d.ts +0 -30
  81. package/dist/deprecated.js +0 -28
  82. package/dist/interpreter/execute-sub-harness.d.ts +0 -8
  83. package/dist/interpreter/execute-sub-harness.js +0 -241
  84. package/dist/interpreter/sub-harness-events.d.ts +0 -48
  85. package/dist/interpreter/sub-harness-events.js +0 -208
  86. package/dist/patterns/interview.d.ts +0 -53
  87. package/dist/patterns/interview.js +0 -113
  88. package/dist/patterns/plans.d.ts +0 -44
  89. package/dist/patterns/plans.js +0 -125
  90. package/dist/patterns/ralph-wiggum.d.ts +0 -19
  91. package/dist/patterns/ralph-wiggum.js +0 -37
  92. package/dist/patterns/react.d.ts +0 -19
  93. package/dist/patterns/react.js +0 -41
  94. /package/dist/{runtime → util}/detached-handle.d.ts +0 -0
@@ -1,30 +0,0 @@
1
- /**
2
- * Deprecated aliases from the "memory layer" era of the framework.
3
- *
4
- * The layer system assembles the model's *context window* — recall, budget
5
- * allocation, history projection, the item-append pipeline — so it is named
6
- * after context, not memory. Every name below still resolves to its
7
- * replacement; nothing here changes behaviour.
8
- *
9
- * Most of these are plain pass-throughs: the alias and its replacement already
10
- * live in `@noetic-tools/types` / `@noetic-tools/context`, and re-exporting
11
- * carries each name's `@deprecated` pointer along with it. Only the renaming
12
- * aliases — where core exports a symbol under a *different* name than its
13
- * source package does — are declared here.
14
- *
15
- * The matching runtime aliases (the `memory:` config key on `provide`/`spawn`/
16
- * `tool`/`react`/`AgentHarness`, and the `ctx.memory` accessor) are handled at
17
- * their call sites — see `resolveContextOption`.
18
- *
19
- * Scheduled for removal in the next major of `@noetic-tools/core`.
20
- */
21
- export type { ObservationalMemoryConfig, PlanMemoryConfig, TemporalMemoryConfig, WorkingMemoryConfig, WorkingMemoryState, } from '@noetic-tools/context';
22
- export { observationalMemory, planMemory, temporalMemory, toolMemoryLayer, workingMemory, } from '@noetic-tools/context';
23
- export type { ContextMemory, InferMemory, InferMemoryShape, MemoryCallModelRequest, MemoryConfig, MemoryHooks, MemoryLayer, MemoryScope, MemoryTraceSpan, ToolMemory, ToolMemoryDeclaration, } from '@noetic-tools/types';
24
- export {
25
- /**
26
- * @public
27
- * @deprecated Renamed to `context`. Declared here rather than re-exported
28
- * because the builder itself lives in core under its new name.
29
- */
30
- context as memory, } from './builders/context-builder.js';
@@ -1,28 +0,0 @@
1
- /**
2
- * Deprecated aliases from the "memory layer" era of the framework.
3
- *
4
- * The layer system assembles the model's *context window* — recall, budget
5
- * allocation, history projection, the item-append pipeline — so it is named
6
- * after context, not memory. Every name below still resolves to its
7
- * replacement; nothing here changes behaviour.
8
- *
9
- * Most of these are plain pass-throughs: the alias and its replacement already
10
- * live in `@noetic-tools/types` / `@noetic-tools/context`, and re-exporting
11
- * carries each name's `@deprecated` pointer along with it. Only the renaming
12
- * aliases — where core exports a symbol under a *different* name than its
13
- * source package does — are declared here.
14
- *
15
- * The matching runtime aliases (the `memory:` config key on `provide`/`spawn`/
16
- * `tool`/`react`/`AgentHarness`, and the `ctx.memory` accessor) are handled at
17
- * their call sites — see `resolveContextOption`.
18
- *
19
- * Scheduled for removal in the next major of `@noetic-tools/core`.
20
- */
21
- export { observationalMemory, planMemory, temporalMemory, toolMemoryLayer, workingMemory, } from '@noetic-tools/context';
22
- export {
23
- /**
24
- * @public
25
- * @deprecated Renamed to `context`. Declared here rather than re-exported
26
- * because the builder itself lives in core under its new name.
27
- */
28
- context as memory, } from './builders/context-builder.js';
@@ -1,8 +0,0 @@
1
- /**
2
- * SubHarness step handler: drives an external coding-agent harness
3
- * (Claude Code, Codex, opencode, pi) for one agentic turn and folds its output
4
- * back into the Noetic execution context — the harness analogue of
5
- * `executeLLM`.
6
- */
7
- import type { Context, StepSubHarness } from '@noetic-tools/types';
8
- export declare function executeSubHarness<TContext, I, O>(step: StepSubHarness<TContext, I, O>, input: I, ctx: Context<TContext>): Promise<O>;
@@ -1,241 +0,0 @@
1
- /**
2
- * SubHarness step handler: drives an external coding-agent harness
3
- * (Claude Code, Codex, opencode, pi) for one agentic turn and folds its output
4
- * back into the Noetic execution context — the harness analogue of
5
- * `executeLLM`.
6
- */
7
- import { createMessage, extractAssistantText, frameworkCast, NoeticConfigError, NoeticErrorImpl, } from '@noetic-tools/types';
8
- import { ZodError } from 'zod';
9
- import { resolveLazy } from './execute-action.js';
10
- import { trackUsage } from './message-helpers.js';
11
- import { SubHarnessEventBridge } from './sub-harness-events.js';
12
- import { isContextImpl, isFunctionCall, isMutableContext } from './typeguards.js';
13
- //#endregion
14
- //#region Helpers
15
- function sessionStore(ctx) {
16
- return frameworkCast(ctx.harness).subHarnessSessions;
17
- }
18
- async function resolveSubHarness(step, ctx) {
19
- const resolved = await resolveLazy(step.harness, ctx);
20
- if (!resolved) {
21
- throw new NoeticConfigError({
22
- code: 'MISSING_SUB_HARNESS',
23
- message: `step.${step.kind}(${JSON.stringify(step.id)}) resolved no harness adapter.`,
24
- hint: 'Pass a harness factory result, e.g. harness: claudeCode({ model }).',
25
- });
26
- }
27
- if (resolved.harnessId !== step.kind) {
28
- throw new NoeticConfigError({
29
- code: 'SUB_HARNESS_KIND_MISMATCH',
30
- message: `step.${step.kind}(${JSON.stringify(step.id)}) was given a '${resolved.harnessId}' harness.`,
31
- hint: `Use the matching builder for this adapter, e.g. step.${resolved.harnessId}({ ... }).`,
32
- });
33
- }
34
- return resolved;
35
- }
36
- function resolveTurnText(resolvedPrompt, input) {
37
- if (resolvedPrompt && resolvedPrompt.length > 0) {
38
- return resolvedPrompt;
39
- }
40
- return typeof input === 'string' ? input : '';
41
- }
42
- /**
43
- * Abort signal of the executing context, when it is a framework context. Handed
44
- * to the adapter so `ctx.abort()` / `harness.cancel()` stops the coding agent's
45
- * in-flight turn instead of leaving a sub-process running until it finishes.
46
- */
47
- function abortSignalOf(ctx) {
48
- return isContextImpl(ctx) ? ctx.abortSignal : undefined;
49
- }
50
- function buildRunContext(ctx) {
51
- return {
52
- cwd: ctx.cwdState.cwd,
53
- fs: ctx.fs,
54
- shell: ctx.shell,
55
- subprocess: ctx.subprocess,
56
- threadId: ctx.threadId,
57
- signal: abortSignalOf(ctx),
58
- };
59
- }
60
- function toLlmResponse(result) {
61
- const usage = result.usage;
62
- return {
63
- items: result.items,
64
- usage: {
65
- inputTokens: usage?.input ?? 0,
66
- outputTokens: usage?.output ?? 0,
67
- cachedTokens: usage?.cached,
68
- },
69
- cost: result.cost,
70
- };
71
- }
72
- function applyTurnResult(ctx, result) {
73
- const toolCalls = [];
74
- for (const item of result.items) {
75
- ctx.itemLog.append(item);
76
- if (isFunctionCall(item)) {
77
- toolCalls.push(item);
78
- }
79
- }
80
- const llmResponse = toLlmResponse(result);
81
- trackUsage(ctx, llmResponse);
82
- const meta = {
83
- toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
84
- usage: result.usage ? llmResponse.usage : undefined,
85
- cost: result.cost,
86
- responseItems: result.items,
87
- };
88
- if (isMutableContext(ctx)) {
89
- ctx.lastStepMeta = meta;
90
- }
91
- }
92
- async function teardownSession(session, mode) {
93
- if (mode === 'detach' && session.doDetach) {
94
- await session.doDetach();
95
- return;
96
- }
97
- if (mode === 'destroy' && session.doDestroy) {
98
- await session.doDestroy();
99
- return;
100
- }
101
- await session.doStop();
102
- }
103
- async function startOrReuseSession(step, harness, ctx, baseCtx, history) {
104
- const reuseKey = step.session?.reuse;
105
- const store = sessionStore(baseCtx);
106
- if (reuseKey) {
107
- const existing = store.get(reuseKey);
108
- if (existing) {
109
- return {
110
- session: existing,
111
- reuseKey,
112
- };
113
- }
114
- }
115
- const session = await harness.doStart({
116
- settings: step.settings,
117
- instructions: await resolveLazy(step.instructions, ctx),
118
- history,
119
- ctx: buildRunContext(baseCtx),
120
- signal: abortSignalOf(baseCtx),
121
- });
122
- if (reuseKey) {
123
- store.set(reuseKey, session);
124
- }
125
- return {
126
- session,
127
- reuseKey,
128
- };
129
- }
130
- /**
131
- * Tear the session down per policy after a successful turn. Reused sessions
132
- * stay alive by default; an explicit `onComplete` overrides that.
133
- */
134
- async function finalizeSession(resolution, policy, baseCtx) {
135
- const { session, reuseKey } = resolution;
136
- if (!reuseKey) {
137
- await teardownSession(session, policy?.onComplete ?? 'stop');
138
- return;
139
- }
140
- const mode = policy?.onComplete;
141
- if (mode === 'stop' || mode === 'destroy') {
142
- await teardownSession(session, mode);
143
- sessionStore(baseCtx).delete(reuseKey);
144
- return;
145
- }
146
- if (mode === 'detach') {
147
- await teardownSession(session, 'detach');
148
- }
149
- // Default for a reused session: keep it alive in the store.
150
- }
151
- //#endregion
152
- //#region Public API
153
- export async function executeSubHarness(step, input, ctx) {
154
- const baseCtx = frameworkCast(ctx);
155
- const harness = await resolveSubHarness(step, ctx);
156
- const resolvedPrompt = await resolveLazy(step.prompt, ctx);
157
- const turnText = resolveTurnText(resolvedPrompt, input);
158
- if (turnText.trim() === '') {
159
- throw new NoeticConfigError({
160
- code: 'MISSING_PROMPT',
161
- message: `step.${step.kind}(${JSON.stringify(step.id)}) resolved an empty prompt.`,
162
- hint: 'Provide a non-empty `prompt`, or pass a string input to the step.',
163
- });
164
- }
165
- // Capture the conversation so far (from earlier LLM/sub-harness steps and
166
- // turns) BEFORE appending this turn's prompt, so a fresh session is seeded
167
- // with full context of the conversation.
168
- const priorHistory = [
169
- ...baseCtx.itemLog.items,
170
- ];
171
- baseCtx.itemLog.append(createMessage(turnText, 'user'));
172
- const resolution = await startOrReuseSession(step, harness, ctx, baseCtx, priorHistory);
173
- const bridge = new SubHarnessEventBridge(step, baseCtx);
174
- bridge.begin();
175
- let result;
176
- try {
177
- result = await resolution.session.doPromptTurn({
178
- prompt: turnText,
179
- emit: (part) => bridge.forward(part),
180
- // Per-turn signal, so a session reused across turns is cancelled by the
181
- // context running the CURRENT turn rather than the one that started it.
182
- signal: abortSignalOf(baseCtx),
183
- });
184
- }
185
- catch (e) {
186
- // Best-effort teardown of a fresh session before surfacing the failure;
187
- // reused sessions are left intact for a later step to retry against.
188
- if (!resolution.reuseKey) {
189
- await teardownSession(resolution.session, 'destroy').catch(() => undefined);
190
- }
191
- if (e instanceof NoeticErrorImpl) {
192
- throw e;
193
- }
194
- // An adapter interrupted by the abort signal rejects with whatever its SDK
195
- // throws; the context is the authority on why the turn ended.
196
- if (baseCtx.aborted) {
197
- throw new NoeticErrorImpl({
198
- kind: 'cancelled',
199
- reason: baseCtx.abortReason ?? 'context aborted',
200
- });
201
- }
202
- throw new NoeticErrorImpl({
203
- kind: 'step_failed',
204
- stepId: step.id,
205
- cause: e instanceof Error ? e : new Error(String(e)),
206
- retriesExhausted: false,
207
- });
208
- }
209
- bridge.finalize(result);
210
- applyTurnResult(baseCtx, result);
211
- await finalizeSession(resolution, step.session, baseCtx);
212
- const lastText = result.text.length > 0 ? result.text : extractAssistantText(result.items);
213
- if (step.output) {
214
- try {
215
- const parsed = JSON.parse(lastText);
216
- return step.output.parse(parsed);
217
- }
218
- catch (e) {
219
- if (e instanceof SyntaxError || e instanceof ZodError) {
220
- throw new NoeticErrorImpl({
221
- kind: 'llm_parse_error',
222
- stepId: step.id,
223
- raw: lastText,
224
- schema: step.output,
225
- zodError: e instanceof ZodError
226
- ? e
227
- : new ZodError([
228
- {
229
- code: 'custom',
230
- message: `Invalid JSON: ${e.message}`,
231
- path: [],
232
- },
233
- ]),
234
- });
235
- }
236
- throw e;
237
- }
238
- }
239
- return frameworkCast(lastText);
240
- }
241
- //#endregion
@@ -1,48 +0,0 @@
1
- /**
2
- * Maps a sub-harness turn's output onto the harness's observable event surface.
3
- *
4
- * A sub-harness adapter emits {@link SubHarnessStreamPart}s; this bridge
5
- * translates each one into the same `source: 'sdk'` broadcaster events the
6
- * model-call path emits, so a coding agent's text, reasoning, and tool calls
7
- * flow through `getTextStream()` / `getReasoningStream()` / `getItemStream()` /
8
- * `getFullStream()` exactly like an LLM step's output. It also re-emits the raw
9
- * part as a `sub_harness_event` framework event for harness-native consumers.
10
- *
11
- * `finalize()` guarantees a turn always emits its output: if an adapter returns
12
- * a result without streaming any text/tool parts, the result's content is
13
- * synthesized into events.
14
- */
15
- import type { Context, ContextData, StepSubHarness, SubHarnessStreamPart, SubHarnessTurnResult } from '@noetic-tools/types';
16
- export declare class SubHarnessEventBridge {
17
- private readonly broadcaster?;
18
- private readonly agentName;
19
- private readonly stepId;
20
- private readonly stepKind;
21
- private readonly emitOption;
22
- private started;
23
- private completed;
24
- private nextOutputIndex;
25
- private messageOutputIndex;
26
- private sawText;
27
- private sawToolCall;
28
- constructor(step: Pick<StepSubHarness, 'id' | 'kind' | 'emit'>, ctx: Context<ContextData>);
29
- /**
30
- * Open the turn on the event surface. Emits `response.created` so every turn
31
- * always brackets with a lifecycle marker — even a custom adapter that
32
- * streams nothing still emits output.
33
- */
34
- begin(): void;
35
- /** Forward one stream part: a structured framework event + the mapped sdk events. */
36
- forward(part: SubHarnessStreamPart): void;
37
- /**
38
- * Ensure the turn's output reached the event surface. For adapters that
39
- * return a result without streaming, synthesize events from the result.
40
- */
41
- finalize(result: SubHarnessTurnResult): void;
42
- private sdk;
43
- private start;
44
- private openMessage;
45
- private closeMessage;
46
- private emitToolCall;
47
- private translate;
48
- }
@@ -1,208 +0,0 @@
1
- /**
2
- * Maps a sub-harness turn's output onto the harness's observable event surface.
3
- *
4
- * A sub-harness adapter emits {@link SubHarnessStreamPart}s; this bridge
5
- * translates each one into the same `source: 'sdk'` broadcaster events the
6
- * model-call path emits, so a coding agent's text, reasoning, and tool calls
7
- * flow through `getTextStream()` / `getReasoningStream()` / `getItemStream()` /
8
- * `getFullStream()` exactly like an LLM step's output. It also re-emits the raw
9
- * part as a `sub_harness_event` framework event for harness-native consumers.
10
- *
11
- * `finalize()` guarantees a turn always emits its output: if an adapter returns
12
- * a result without streaming any text/tool parts, the result's content is
13
- * synthesized into events.
14
- */
15
- import { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
16
- import { isFunctionCall } from './typeguards.js';
17
- export class SubHarnessEventBridge {
18
- broadcaster;
19
- agentName;
20
- stepId;
21
- stepKind;
22
- emitOption;
23
- started = false;
24
- completed = false;
25
- nextOutputIndex = 0;
26
- messageOutputIndex = null;
27
- sawText = false;
28
- sawToolCall = false;
29
- constructor(step, ctx) {
30
- this.broadcaster = getBroadcaster(ctx);
31
- this.agentName = ctx.harness.config.name;
32
- this.stepId = step.id;
33
- this.stepKind = step.kind;
34
- this.emitOption = step.emit;
35
- }
36
- /**
37
- * Open the turn on the event surface. Emits `response.created` so every turn
38
- * always brackets with a lifecycle marker — even a custom adapter that
39
- * streams nothing still emits output.
40
- */
41
- begin() {
42
- this.start();
43
- }
44
- /** Forward one stream part: a structured framework event + the mapped sdk events. */
45
- forward(part) {
46
- if (!this.broadcaster) {
47
- return;
48
- }
49
- const data = {
50
- stepId: this.stepId,
51
- kind: this.stepKind,
52
- part,
53
- };
54
- if (shouldEmit(this.emitOption, 'sub_harness_event', data)) {
55
- emitFrameworkEvent({
56
- broadcaster: this.broadcaster,
57
- agentName: this.agentName,
58
- eventType: 'sub_harness_event',
59
- data,
60
- });
61
- }
62
- this.translate(part);
63
- }
64
- /**
65
- * Ensure the turn's output reached the event surface. For adapters that
66
- * return a result without streaming, synthesize events from the result.
67
- */
68
- finalize(result) {
69
- if (!this.broadcaster) {
70
- return;
71
- }
72
- if (!this.sawText && result.text.length > 0) {
73
- this.openMessage();
74
- this.sdk('response.output_text.delta', {
75
- delta: result.text,
76
- }, this.messageOutputIndex ?? 0);
77
- this.sawText = true;
78
- }
79
- if (!this.sawToolCall) {
80
- for (const item of result.items) {
81
- if (isFunctionCall(item)) {
82
- this.emitToolCall(item.callId, item.name, item.arguments);
83
- }
84
- }
85
- }
86
- this.closeMessage();
87
- // Always close the turn so every turn emits a completion marker.
88
- if (!this.completed) {
89
- this.start();
90
- this.sdk('response.completed', {});
91
- this.completed = true;
92
- }
93
- }
94
- //#region internals
95
- sdk(type, data, outputIndex) {
96
- if (!this.broadcaster) {
97
- return;
98
- }
99
- if (!shouldEmit(this.emitOption, type, data)) {
100
- return;
101
- }
102
- this.broadcaster.emit({
103
- source: 'sdk',
104
- type,
105
- data,
106
- outputIndex,
107
- });
108
- }
109
- start() {
110
- if (this.started) {
111
- return;
112
- }
113
- this.started = true;
114
- this.sdk('response.created', {});
115
- }
116
- openMessage() {
117
- if (this.messageOutputIndex !== null) {
118
- return;
119
- }
120
- this.start();
121
- const id = `sub-harness-msg-${crypto.randomUUID()}`;
122
- this.messageOutputIndex = this.nextOutputIndex++;
123
- this.sdk('response.output_item.added', {
124
- item: {
125
- id,
126
- type: 'message',
127
- role: 'assistant',
128
- status: 'in_progress',
129
- content: [
130
- {
131
- type: 'output_text',
132
- text: '',
133
- },
134
- ],
135
- },
136
- }, this.messageOutputIndex);
137
- }
138
- closeMessage() {
139
- if (this.messageOutputIndex === null) {
140
- return;
141
- }
142
- const index = this.messageOutputIndex;
143
- this.sdk('response.output_text.done', {}, index);
144
- this.sdk('response.output_item.done', {}, index);
145
- this.messageOutputIndex = null;
146
- }
147
- emitToolCall(toolCallId, name, input) {
148
- this.start();
149
- const index = this.nextOutputIndex++;
150
- const args = typeof input === 'string' ? input : JSON.stringify(input ?? {});
151
- this.sdk('response.output_item.added', {
152
- item: {
153
- id: toolCallId,
154
- type: 'function_call',
155
- status: 'in_progress',
156
- callId: toolCallId,
157
- name,
158
- arguments: '',
159
- },
160
- }, index);
161
- this.sdk('response.function_call_arguments.delta', {
162
- delta: args,
163
- }, index);
164
- this.sdk('response.function_call_arguments.done', {
165
- arguments: args,
166
- }, index);
167
- this.sdk('response.output_item.done', {}, index);
168
- this.sawToolCall = true;
169
- }
170
- translate(part) {
171
- if (part.type === 'stream-start') {
172
- this.start();
173
- return;
174
- }
175
- if (part.type === 'text-delta') {
176
- this.openMessage();
177
- this.sdk('response.output_text.delta', {
178
- delta: part.delta,
179
- }, this.messageOutputIndex ?? 0);
180
- this.sawText = true;
181
- return;
182
- }
183
- if (part.type === 'reasoning-delta') {
184
- this.start();
185
- this.sdk('response.reasoning.delta', {
186
- delta: part.delta,
187
- });
188
- return;
189
- }
190
- if (part.type === 'tool-call') {
191
- this.closeMessage();
192
- this.emitToolCall(part.toolCallId, part.toolName, part.input);
193
- return;
194
- }
195
- if (part.type === 'finish') {
196
- this.closeMessage();
197
- this.sdk('response.completed', {
198
- finishReason: part.finishReason,
199
- });
200
- this.completed = true;
201
- return;
202
- }
203
- // file-change | tool-result | error | raw — surfaced on the full stream only.
204
- this.sdk(`sub_harness.${part.type}`, {
205
- part,
206
- });
207
- }
208
- }
@@ -1,53 +0,0 @@
1
- import type { ContextData } from '@noetic-tools/context';
2
- import type { Step } from '@noetic-tools/types';
3
- import type { ZodType } from 'zod';
4
- /** @public Answer captured by the host for a single interview question. */
5
- export interface InterviewQuestionAnswer {
6
- questionId: string;
7
- question: string;
8
- answer: string | string[];
9
- notes?: string;
10
- }
11
- /** @public Result of running an `interview()` step. */
12
- export type InterviewResult<Q, C> = {
13
- status: 'complete';
14
- envelope: C;
15
- } | {
16
- status: 'maxQuestions';
17
- lastQuestion?: Q;
18
- };
19
- /** @public Configuration for the `interview()` pattern. */
20
- export interface InterviewOpts<Q, C> {
21
- /** System prompt instructing the model on how to interview the user. */
22
- systemPrompt: string;
23
- /** Model identifier passed to `step.llm`. */
24
- model: string;
25
- /** Zod schema for the `data` payload of a `question` envelope. */
26
- questionSchema: ZodType<Q>;
27
- /** Zod schema for the `data` payload of a `complete` envelope. */
28
- completeSchema: ZodType<C>;
29
- /** Host renders the question (e.g. via AskUserService) and returns the answer. */
30
- askQuestion: (envelope: Q) => Promise<InterviewQuestionAnswer>;
31
- /** Called once when the model emits the complete envelope. */
32
- onComplete: (envelope: C) => Promise<void>;
33
- /** Hard cap on question/complete turns before the loop exits with `maxQuestions`. Default 8. */
34
- maxQuestions?: number;
35
- /**
36
- * Override how a captured answer becomes the next user message. Default emits a
37
- * compact, machine-readable JSON line that the model can parse to build context.
38
- */
39
- formatAnswer?: (answer: InterviewQuestionAnswer) => string;
40
- }
41
- /**
42
- * Creates a host-callback-driven structured interview pattern. Internally a loop over a single
43
- * body step that delegates to `step.llm` (with `output: discriminatedUnion('type', [...])`) and
44
- * then hands the envelope to the host. `askQuestion` renders questions and returns answers fed
45
- * back as the next user message; `onComplete` fires once when the model emits `complete`.
46
- * Termination via `until.verified(complete) || until.maxSteps(maxQuestions)`.
47
- *
48
- * @public
49
- * @param opts - System prompt, model, question/complete schemas, host callbacks, optional caps.
50
- * @returns A `Step` whose output is `InterviewResult<Q, C>` — either the completion envelope or
51
- * `maxQuestions` when the budget is exhausted before the model emits `complete`.
52
- */
53
- export declare function interview<Q, C>(opts: InterviewOpts<Q, C>): Step<ContextData, string, InterviewResult<Q, C>>;