@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.
- package/dist/builders/step-builders.d.ts +51 -1
- package/dist/builders/step-builders.js +81 -0
- package/dist/builders/workflow-hydrator.d.ts +3 -1
- package/dist/builders/workflow-hydrator.js +34 -0
- package/dist/harness/agent-harness.d.ts +8 -1
- package/dist/harness/agent-harness.js +7 -0
- package/dist/harness/deps/types.d.ts +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -0
- package/dist/interpreter/collect-tools.js +6 -0
- package/dist/interpreter/execute-action.d.ts +10 -0
- package/dist/interpreter/execute-action.js +1 -1
- package/dist/interpreter/execute-sub-harness.d.ts +8 -0
- package/dist/interpreter/execute-sub-harness.js +220 -0
- package/dist/interpreter/execute.js +22 -1
- package/dist/interpreter/sub-harness-events.d.ts +48 -0
- package/dist/interpreter/sub-harness-events.js +208 -0
- package/dist/patterns/dynamic-workflow.js +3 -0
- package/dist/schemas/workflow.d.ts +35 -2
- package/dist/schemas/workflow.js +46 -1
- package/package.json +2 -2
- package/schema/noetic-workflow.schema.json +320 -0
|
@@ -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
|
|
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 {
|
package/dist/schemas/workflow.js
CHANGED
|
@@ -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
|
-
/**
|
|
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.
|
|
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.
|
|
70
|
+
"@noetic-tools/types": "^1.0.1",
|
|
71
71
|
"@openrouter/agent": "^0.6.0",
|
|
72
72
|
"zod": "^4.0.0"
|
|
73
73
|
},
|