@noetic-tools/core 1.1.0 → 1.1.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.
@@ -0,0 +1,48 @@
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, ContextMemory, 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<ContextMemory>);
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
+ }
@@ -0,0 +1,208 @@
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
+ }
@@ -27,6 +27,9 @@ A WorkflowNode is one of:
27
27
  - { "kind": "loop", "id": "<unique>", "body": <WorkflowNode>, "until": { "kind": "maxSteps", "n": <number> } }
28
28
  - { "kind": "branch", "id": "<unique>", "routes": [{ "match": "<substring>", "target": <WorkflowNode> }], "default": <WorkflowNode> }
29
29
  - { "kind": "spawn", "id": "<unique>", "child": <WorkflowNode> }
30
+ - { "kind": "claude-code"|"codex"|"opencode"|"pi", "id": "<unique>", "prompt": "<turn prompt>", "settings": { "model": "<optional>", "permissionMode": "<optional>" } }
31
+
32
+ SubHarness nodes (claude-code, codex, opencode, pi) delegate a turn to an external coding agent; only emit one if a matching harness adapter is registered for the workflow.
30
33
 
31
34
  Until predicates: maxSteps, maxCost, maxDuration, noToolCalls, outputContains, outputEquals, converged, any, all.
32
35
 
@@ -6,6 +6,7 @@
6
6
  * closures). The hydrator in `builders/workflow-hydrator.ts` converts a
7
7
  * validated document back into live `Step` objects via the existing builders.
8
8
  */
9
+ import type { SubHarnessKind } from '@noetic-tools/types';
9
10
  import { z } from 'zod';
10
11
  interface UntilAnyPredicate {
11
12
  kind: 'any';
@@ -59,6 +60,26 @@ declare const ModelParamsSchema: z.ZodObject<{
59
60
  maxTokens: z.ZodOptional<z.ZodNumber>;
60
61
  stopSequences: z.ZodOptional<z.ZodArray<z.ZodString>>;
61
62
  }, z.core.$strip>;
63
+ declare const HarnessSettingsSchema: z.ZodObject<{
64
+ model: z.ZodOptional<z.ZodString>;
65
+ permissionMode: z.ZodOptional<z.ZodEnum<{
66
+ default: "default";
67
+ plan: "plan";
68
+ acceptEdits: "acceptEdits";
69
+ bypassPermissions: "bypassPermissions";
70
+ }>>;
71
+ maxTurns: z.ZodOptional<z.ZodNumber>;
72
+ allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
73
+ extra: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
74
+ }, z.core.$strip>;
75
+ declare const HarnessSessionPolicySchema: z.ZodObject<{
76
+ reuse: z.ZodOptional<z.ZodString>;
77
+ onComplete: z.ZodOptional<z.ZodEnum<{
78
+ stop: "stop";
79
+ detach: "detach";
80
+ destroy: "destroy";
81
+ }>>;
82
+ }, z.core.$strip>;
62
83
  interface WorkflowNodeBase {
63
84
  id: string;
64
85
  }
@@ -116,9 +137,21 @@ export interface EveryWorkflowNode extends WorkflowNodeBase {
116
137
  ms: number;
117
138
  onError?: 'continue' | 'fail';
118
139
  }
140
+ /**
141
+ * A node that delegates a turn to a coding-agent harness. `kind` is the harness
142
+ * id (e.g. `claude-code`); the hydrator resolves the matching adapter from the
143
+ * workflow's harness registry.
144
+ */
145
+ export interface SubHarnessWorkflowNode extends WorkflowNodeBase {
146
+ kind: SubHarnessKind;
147
+ prompt: string;
148
+ instructions?: string;
149
+ settings?: z.infer<typeof HarnessSettingsSchema>;
150
+ session?: z.infer<typeof HarnessSessionPolicySchema>;
151
+ }
119
152
  /** @public Discriminated union of all JSON-serialisable workflow node kinds. */
120
- export type WorkflowNode = LlmWorkflowNode | ToolWorkflowNode | BranchWorkflowNode | ForkWorkflowNode | SpawnWorkflowNode | ProvideWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | EveryWorkflowNode;
121
- /** @public Zod schema validating a single `WorkflowNode` (any of the 9 JSON-safe kinds). */
153
+ export type WorkflowNode = LlmWorkflowNode | ToolWorkflowNode | BranchWorkflowNode | ForkWorkflowNode | SpawnWorkflowNode | ProvideWorkflowNode | LoopWorkflowNode | SequenceWorkflowNode | EveryWorkflowNode | SubHarnessWorkflowNode;
154
+ /** @public Zod schema validating a single `WorkflowNode` (any JSON-safe kind). */
122
155
  export declare const WorkflowNodeSchema: z.ZodType<WorkflowNode>;
123
156
  /** @public Top-level envelope for a JSON workflow definition. */
124
157
  export interface WorkflowDocument {
@@ -85,6 +85,32 @@ const ModelParamsSchema = z.object({
85
85
  stopSequences: z.array(z.string()).optional(),
86
86
  });
87
87
  //#endregion
88
+ //#region SubHarness Settings
89
+ const HarnessSettingsSchema = z.object({
90
+ model: z.string().optional(),
91
+ permissionMode: z
92
+ .enum([
93
+ 'default',
94
+ 'plan',
95
+ 'acceptEdits',
96
+ 'bypassPermissions',
97
+ ])
98
+ .optional(),
99
+ maxTurns: z.number().int().positive().optional(),
100
+ allowedTools: z.array(z.string()).optional(),
101
+ extra: z.record(z.string(), z.unknown()).optional(),
102
+ });
103
+ const HarnessSessionPolicySchema = z.object({
104
+ reuse: z.string().min(1).optional(),
105
+ onComplete: z
106
+ .enum([
107
+ 'stop',
108
+ 'detach',
109
+ 'destroy',
110
+ ])
111
+ .optional(),
112
+ });
113
+ //#endregion
88
114
  //#region Workflow Node Schema
89
115
  const WorkflowNodeRef = z.lazy(() => WorkflowNodeSchema);
90
116
  const SHARED_FIELDS = {
@@ -162,7 +188,22 @@ const EveryNodeSchema = z.object({
162
188
  ])
163
189
  .optional(),
164
190
  });
165
- /** @public Zod schema validating a single `WorkflowNode` (any of the 9 JSON-safe kinds). */
191
+ /** Builds the schema for a single harness node kind (`claude-code`, `codex`, …). */
192
+ function subHarnessNodeSchema(kind) {
193
+ return z.object({
194
+ kind: z.literal(kind),
195
+ ...SHARED_FIELDS,
196
+ prompt: z.string().min(1),
197
+ instructions: z.string().optional(),
198
+ settings: HarnessSettingsSchema.optional(),
199
+ session: HarnessSessionPolicySchema.optional(),
200
+ });
201
+ }
202
+ const ClaudeCodeNodeSchema = subHarnessNodeSchema('claude-code');
203
+ const CodexNodeSchema = subHarnessNodeSchema('codex');
204
+ const OpencodeNodeSchema = subHarnessNodeSchema('opencode');
205
+ const PiNodeSchema = subHarnessNodeSchema('pi');
206
+ /** @public Zod schema validating a single `WorkflowNode` (any JSON-safe kind). */
166
207
  export const WorkflowNodeSchema = z
167
208
  .discriminatedUnion('kind', [
168
209
  LlmNodeSchema,
@@ -174,6 +215,10 @@ export const WorkflowNodeSchema = z
174
215
  LoopNodeSchema,
175
216
  SequenceNodeSchema,
176
217
  EveryNodeSchema,
218
+ ClaudeCodeNodeSchema,
219
+ CodexNodeSchema,
220
+ OpencodeNodeSchema,
221
+ PiNodeSchema,
177
222
  ])
178
223
  .meta({
179
224
  id: 'WorkflowNode',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noetic-tools/core",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "type": "module",
5
5
  "description": "Step primitives, interpreter, runtime, and memory layer contract for Noetic agents.",
6
6
  "license": "Apache-2.0",
@@ -67,7 +67,7 @@
67
67
  },
68
68
  "dependencies": {
69
69
  "@noetic-tools/memory": "^1.0.1",
70
- "@noetic-tools/types": "^1.0.0",
70
+ "@noetic-tools/types": "^1.0.1",
71
71
  "@openrouter/agent": "^0.6.0",
72
72
  "zod": "^4.0.0"
73
73
  },