@noetic-tools/core 1.2.1 → 1.2.3
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/harness/agent-harness.js +1 -0
- package/dist/harness/model-call.d.ts +4 -1
- package/dist/harness/model-call.js +104 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/interpreter/execute-action.js +4 -0
- package/dist/observability/genai-attributes.d.ts +14 -0
- package/dist/observability/genai-attributes.js +16 -0
- package/dist/patterns/dynamic-workflow.js +34 -3
- package/dist/schemas/workflow.d.ts +17 -0
- package/dist/schemas/workflow.js +25 -0
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentHarnessContract, CallModelRequest, ItemSchemaRegistry, LLMResponse } from '@noetic-tools/types';
|
|
1
|
+
import type { AgentHarnessContract, CallModelRequest, ItemSchemaRegistry, LLMResponse, TraceExporter } from '@noetic-tools/types';
|
|
2
2
|
import type { OpenRouter } from '@openrouter/agent';
|
|
3
3
|
import type { MessageQueue } from '../runtime/message-queue.js';
|
|
4
4
|
interface StreamIdleWatchdog {
|
|
@@ -28,13 +28,16 @@ interface AgentHarnessModelCallerOpts {
|
|
|
28
28
|
callModelOverride?: (request: CallModelRequest) => Promise<LLMResponse>;
|
|
29
29
|
streamIdleTimeoutMs: number;
|
|
30
30
|
harness: AgentHarnessContract;
|
|
31
|
+
traceExporter: TraceExporter;
|
|
31
32
|
}
|
|
32
33
|
export declare class AgentHarnessModelCaller {
|
|
33
34
|
private readonly opts;
|
|
34
35
|
constructor(opts: AgentHarnessModelCallerOpts);
|
|
36
|
+
private newSpanCollector;
|
|
35
37
|
private callOverriddenModel;
|
|
36
38
|
private requireModelClient;
|
|
37
39
|
callModel(request: CallModelRequest): Promise<LLMResponse>;
|
|
40
|
+
private runModelLoop;
|
|
38
41
|
private injectBetweenRoundMessages;
|
|
39
42
|
private handleModelRecovery;
|
|
40
43
|
private handleEmptyOutputRecovery;
|
|
@@ -2,6 +2,7 @@ import { frameworkCast, NoeticConfigError } from '@noetic-tools/types';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { convertTools, executeToolCall, extractOutputItems, extractSystemInstruction, extractUsage, itemsToInput, sanitizeToolNameForWire, } from '../adapters/openrouter.js';
|
|
4
4
|
import { isFunctionCall } from '../interpreter/typeguards.js';
|
|
5
|
+
import { GenAI, NoeticAttr, ToolAttr } from '../observability/genai-attributes.js';
|
|
5
6
|
import { emitFrameworkEvent, getBroadcaster, shouldEmit } from '../runtime/broadcaster-utils.js';
|
|
6
7
|
import { buildItemSchemaRegistry, createToolResultItem } from './model-schema.js';
|
|
7
8
|
const MAX_TOOL_ROUNDS = 32;
|
|
@@ -219,25 +220,95 @@ function filterAllowedTools(request) {
|
|
|
219
220
|
const allowedNames = new Set(request.allowedToolNames);
|
|
220
221
|
return request.tools.filter((tool) => allowedNames.has(tool.name));
|
|
221
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Per-`callModel` span accumulator. Spans are created lazily from the calling
|
|
225
|
+
* context's span (so the trace tree mirrors the execution tree) and flushed to
|
|
226
|
+
* the harness `traceExporter` once the call settles — see issue #50, where the
|
|
227
|
+
* exporter was stored but never invoked.
|
|
228
|
+
*/
|
|
229
|
+
class CallSpanCollector {
|
|
230
|
+
harness;
|
|
231
|
+
exporter;
|
|
232
|
+
parent;
|
|
233
|
+
nodeId;
|
|
234
|
+
spans = [];
|
|
235
|
+
constructor(harness, exporter, parent, nodeId) {
|
|
236
|
+
this.harness = harness;
|
|
237
|
+
this.exporter = exporter;
|
|
238
|
+
this.parent = parent;
|
|
239
|
+
this.nodeId = nodeId;
|
|
240
|
+
}
|
|
241
|
+
/** Link a span back to its declared workflow node, when the call carries one. */
|
|
242
|
+
stampNode(span) {
|
|
243
|
+
if (this.nodeId !== undefined) {
|
|
244
|
+
span.setAttribute(NoeticAttr.NODE_ID, this.nodeId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** Open a model-call span stamped with GenAI semantic-convention attributes. */
|
|
248
|
+
startModelSpan(model) {
|
|
249
|
+
const span = this.harness.createSpan('llm.call', this.parent);
|
|
250
|
+
span.setAttribute(GenAI.SYSTEM, 'openrouter');
|
|
251
|
+
span.setAttribute(GenAI.REQUEST_MODEL, model);
|
|
252
|
+
this.stampNode(span);
|
|
253
|
+
this.spans.push(span);
|
|
254
|
+
return span;
|
|
255
|
+
}
|
|
256
|
+
/** Record usage + cost on a model-call span. */
|
|
257
|
+
finishModelSpan(span, usage, cost) {
|
|
258
|
+
span.setAttribute(GenAI.USAGE_INPUT_TOKENS, usage.inputTokens);
|
|
259
|
+
span.setAttribute(GenAI.USAGE_OUTPUT_TOKENS, usage.outputTokens);
|
|
260
|
+
if (cost !== undefined) {
|
|
261
|
+
span.setAttribute(GenAI.COST, cost);
|
|
262
|
+
}
|
|
263
|
+
span.end();
|
|
264
|
+
}
|
|
265
|
+
/** Open and immediately close a tool-call span under the given model span. */
|
|
266
|
+
recordToolSpan(parent, toolName, needsApproval) {
|
|
267
|
+
const span = this.harness.createSpan('tool.call', parent);
|
|
268
|
+
span.setAttribute(ToolAttr.NAME, toolName);
|
|
269
|
+
span.setAttribute(ToolAttr.NEEDS_APPROVAL, needsApproval);
|
|
270
|
+
this.stampNode(span);
|
|
271
|
+
span.end();
|
|
272
|
+
this.spans.push(span);
|
|
273
|
+
}
|
|
274
|
+
/** Flush all accumulated spans to the exporter. */
|
|
275
|
+
async flush() {
|
|
276
|
+
if (this.spans.length === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
await this.exporter.export(this.spans);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
222
282
|
export class AgentHarnessModelCaller {
|
|
223
283
|
opts;
|
|
224
284
|
constructor(opts) {
|
|
225
285
|
this.opts = opts;
|
|
226
286
|
}
|
|
287
|
+
newSpanCollector(request) {
|
|
288
|
+
return new CallSpanCollector(this.opts.harness, this.opts.traceExporter, request.parentSpan ?? request.ctx?.span ?? null, request.nodeId);
|
|
289
|
+
}
|
|
227
290
|
async callOverriddenModel(request) {
|
|
228
291
|
if (!this.opts.callModelOverride) {
|
|
229
292
|
throw new Error('No callModel override configured.');
|
|
230
293
|
}
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
294
|
+
const spans = this.newSpanCollector(request);
|
|
295
|
+
const modelSpan = spans.startModelSpan(request.model);
|
|
296
|
+
try {
|
|
297
|
+
const response = await this.opts.callModelOverride(request);
|
|
298
|
+
const itemSchemas = buildItemSchemaRegistry({
|
|
299
|
+
base: this.opts.itemSchemas,
|
|
300
|
+
layers: request.layers,
|
|
301
|
+
tools: request.tools,
|
|
302
|
+
});
|
|
303
|
+
spans.finishModelSpan(modelSpan, response.usage, response.cost);
|
|
304
|
+
return {
|
|
305
|
+
...response,
|
|
306
|
+
items: itemSchemas.parseMany(response.items),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
await spans.flush();
|
|
311
|
+
}
|
|
241
312
|
}
|
|
242
313
|
requireModelClient() {
|
|
243
314
|
if (this.opts.client) {
|
|
@@ -254,6 +325,15 @@ export class AgentHarnessModelCaller {
|
|
|
254
325
|
return this.callOverriddenModel(request);
|
|
255
326
|
}
|
|
256
327
|
const client = this.requireModelClient();
|
|
328
|
+
const spans = this.newSpanCollector(request);
|
|
329
|
+
try {
|
|
330
|
+
return await this.runModelLoop(request, client, spans);
|
|
331
|
+
}
|
|
332
|
+
finally {
|
|
333
|
+
await spans.flush();
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
async runModelLoop(request, client, spans) {
|
|
257
337
|
const prepared = prepareModelRequest(request, this.opts.agentName);
|
|
258
338
|
const allItems = [];
|
|
259
339
|
const totalUsage = {
|
|
@@ -299,6 +379,7 @@ export class AgentHarnessModelCaller {
|
|
|
299
379
|
toolCount: prepared.sdkTools?.length ?? 0,
|
|
300
380
|
recoveryContinuation,
|
|
301
381
|
});
|
|
382
|
+
const modelSpan = spans.startModelSpan(request.model);
|
|
302
383
|
const callResult = client.callModel({
|
|
303
384
|
model: request.model,
|
|
304
385
|
input: modelInput,
|
|
@@ -360,6 +441,7 @@ export class AgentHarnessModelCaller {
|
|
|
360
441
|
emitIfAllowed: prepared.emitIfAllowed,
|
|
361
442
|
});
|
|
362
443
|
if (recovery.kind === 'continue') {
|
|
444
|
+
modelSpan.end();
|
|
363
445
|
invalidRecoveryContinuations = recovery.invalidRecoveryContinuations;
|
|
364
446
|
useEphemeralContinue = true;
|
|
365
447
|
continue;
|
|
@@ -378,6 +460,7 @@ export class AgentHarnessModelCaller {
|
|
|
378
460
|
emitIfAllowed: prepared.emitIfAllowed,
|
|
379
461
|
});
|
|
380
462
|
if (outputRecovery.kind === 'continue') {
|
|
463
|
+
modelSpan.end();
|
|
381
464
|
invalidRecoveryContinuations = outputRecovery.invalidRecoveryContinuations;
|
|
382
465
|
useEphemeralContinue = true;
|
|
383
466
|
continue;
|
|
@@ -388,10 +471,15 @@ export class AgentHarnessModelCaller {
|
|
|
388
471
|
itemCount: sdkResponse.output?.length ?? 0,
|
|
389
472
|
});
|
|
390
473
|
const roundUsage = extractUsage(sdkResponse.usage);
|
|
474
|
+
const roundCost = sdkResponse.usage?.cost ?? 0;
|
|
475
|
+
spans.finishModelSpan(modelSpan, {
|
|
476
|
+
inputTokens: roundUsage.inputTokens,
|
|
477
|
+
outputTokens: roundUsage.outputTokens,
|
|
478
|
+
}, roundCost > 0 ? roundCost : undefined);
|
|
391
479
|
totalUsage.inputTokens += roundUsage.inputTokens;
|
|
392
480
|
totalUsage.outputTokens += roundUsage.outputTokens;
|
|
393
481
|
totalUsage.cachedTokens += roundUsage.cachedTokens ?? 0;
|
|
394
|
-
totalCost +=
|
|
482
|
+
totalCost += roundCost;
|
|
395
483
|
allItems.push(...roundItems);
|
|
396
484
|
const functionCalls = roundItems.filter(isFunctionCall);
|
|
397
485
|
if (functionCalls.length === 0 || !request.tools) {
|
|
@@ -404,6 +492,8 @@ export class AgentHarnessModelCaller {
|
|
|
404
492
|
conversationInput,
|
|
405
493
|
round,
|
|
406
494
|
emitIfAllowed: prepared.emitIfAllowed,
|
|
495
|
+
spans,
|
|
496
|
+
modelSpan,
|
|
407
497
|
});
|
|
408
498
|
round += 1;
|
|
409
499
|
const limitRecovery = this.handleToolRoundLimit({
|
|
@@ -516,7 +606,7 @@ export class AgentHarnessModelCaller {
|
|
|
516
606
|
};
|
|
517
607
|
}
|
|
518
608
|
async executeToolRound(params) {
|
|
519
|
-
const { functionCalls, request, allItems, conversationInput } = params;
|
|
609
|
+
const { functionCalls, request, allItems, conversationInput, spans, modelSpan } = params;
|
|
520
610
|
params.emitIfAllowed('tool_round_started', {
|
|
521
611
|
round: params.round,
|
|
522
612
|
toolCount: functionCalls.length,
|
|
@@ -534,6 +624,8 @@ export class AgentHarnessModelCaller {
|
|
|
534
624
|
if (request.signal?.aborted) {
|
|
535
625
|
break;
|
|
536
626
|
}
|
|
627
|
+
const toolForCall = request.tools?.find((tool) => tool.name === fc.name || sanitizeToolNameForWire(tool.name) === fc.name);
|
|
628
|
+
spans.recordToolSpan(modelSpan, fc.name, toolForCall?.needsApproval ?? false);
|
|
537
629
|
await this.executeFunctionCall({
|
|
538
630
|
fc,
|
|
539
631
|
request,
|
package/dist/index.d.ts
CHANGED
|
@@ -72,7 +72,7 @@ export { durableTaskState, fileReference, findFunctionCall, historyWindow, obser
|
|
|
72
72
|
/** @public */
|
|
73
73
|
export { createMessage, estimateTokens } from '@noetic-tools/types';
|
|
74
74
|
/** @public */
|
|
75
|
-
export { GenAI, ToolAttr } from './observability/genai-attributes.js';
|
|
75
|
+
export { GenAI, NoeticAttr, ToolAttr } from './observability/genai-attributes.js';
|
|
76
76
|
/** @public */
|
|
77
77
|
export { InMemoryExporter, NoopExporter } from './observability/trace-exporter.js';
|
|
78
78
|
/** @public */
|
package/dist/index.js
CHANGED
|
@@ -66,7 +66,7 @@ export { createMessage, estimateTokens } from '@noetic-tools/types';
|
|
|
66
66
|
//#endregion
|
|
67
67
|
//#region Observability
|
|
68
68
|
/** @public */
|
|
69
|
-
export { GenAI, ToolAttr } from './observability/genai-attributes.js';
|
|
69
|
+
export { GenAI, NoeticAttr, ToolAttr } from './observability/genai-attributes.js';
|
|
70
70
|
/** @public */
|
|
71
71
|
export { InMemoryExporter, NoopExporter } from './observability/trace-exporter.js';
|
|
72
72
|
/** @public */
|
|
@@ -293,6 +293,8 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
293
293
|
ctx: baseCtx,
|
|
294
294
|
layers,
|
|
295
295
|
allowedToolNames,
|
|
296
|
+
nodeId: step.id,
|
|
297
|
+
parentSpan: baseCtx.span,
|
|
296
298
|
}
|
|
297
299
|
: {
|
|
298
300
|
model: resolvedModel,
|
|
@@ -301,6 +303,8 @@ export async function executeLLM(step, input, ctx, layers) {
|
|
|
301
303
|
params: step.params,
|
|
302
304
|
outputSchema: step.output,
|
|
303
305
|
emit: step.emit,
|
|
306
|
+
nodeId: step.id,
|
|
307
|
+
parentSpan: baseCtx.span,
|
|
304
308
|
};
|
|
305
309
|
const response = await baseCtx.harness.callModel(request);
|
|
306
310
|
// Track tokens/cost for EVERY model call, including responses steering
|
|
@@ -9,3 +9,17 @@ export declare const ToolAttr: {
|
|
|
9
9
|
readonly NAME: "tool.name";
|
|
10
10
|
readonly NEEDS_APPROVAL: "tool.needs_approval";
|
|
11
11
|
};
|
|
12
|
+
export declare const NoeticAttr: {
|
|
13
|
+
/** Full JSON-serialised `WorkflowDocument` (the DAG, lossless). */
|
|
14
|
+
readonly WORKFLOW_DOCUMENT: "noetic.workflow.document";
|
|
15
|
+
/** Workflow document schema version. */
|
|
16
|
+
readonly WORKFLOW_VERSION: "noetic.workflow.version";
|
|
17
|
+
/** Count of declared nodes in the workflow tree. */
|
|
18
|
+
readonly WORKFLOW_NODE_COUNT: "noetic.workflow.node_count";
|
|
19
|
+
/** JSON array of `{ id, kind }` for every declared node (flattened graph). */
|
|
20
|
+
readonly WORKFLOW_NODES: "noetic.workflow.nodes";
|
|
21
|
+
/** JSON array of `{ from, to }` parent→child edges between declared nodes. */
|
|
22
|
+
readonly WORKFLOW_EDGES: "noetic.workflow.edges";
|
|
23
|
+
/** Id of the declared workflow node an `llm.call`/`tool.call` span belongs to. */
|
|
24
|
+
readonly NODE_ID: "noetic.node.id";
|
|
25
|
+
};
|
|
@@ -10,3 +10,19 @@ export const ToolAttr = {
|
|
|
10
10
|
NAME: 'tool.name',
|
|
11
11
|
NEEDS_APPROVAL: 'tool.needs_approval',
|
|
12
12
|
};
|
|
13
|
+
// Noetic-specific span attributes describing the static workflow graph (the
|
|
14
|
+
// "potential paths" of the DAG) carried on the root `workflow.run` span.
|
|
15
|
+
export const NoeticAttr = {
|
|
16
|
+
/** Full JSON-serialised `WorkflowDocument` (the DAG, lossless). */
|
|
17
|
+
WORKFLOW_DOCUMENT: 'noetic.workflow.document',
|
|
18
|
+
/** Workflow document schema version. */
|
|
19
|
+
WORKFLOW_VERSION: 'noetic.workflow.version',
|
|
20
|
+
/** Count of declared nodes in the workflow tree. */
|
|
21
|
+
WORKFLOW_NODE_COUNT: 'noetic.workflow.node_count',
|
|
22
|
+
/** JSON array of `{ id, kind }` for every declared node (flattened graph). */
|
|
23
|
+
WORKFLOW_NODES: 'noetic.workflow.nodes',
|
|
24
|
+
/** JSON array of `{ from, to }` parent→child edges between declared nodes. */
|
|
25
|
+
WORKFLOW_EDGES: 'noetic.workflow.edges',
|
|
26
|
+
/** Id of the declared workflow node an `llm.call`/`tool.call` span belongs to. */
|
|
27
|
+
NODE_ID: 'noetic.node.id',
|
|
28
|
+
};
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
import { frameworkCast, NoeticConfigError } from '@noetic-tools/types';
|
|
6
6
|
import { step } from '../builders/step-builders.js';
|
|
7
7
|
import { hydrateWorkflow } from '../builders/workflow-hydrator.js';
|
|
8
|
-
import {
|
|
8
|
+
import { NoeticAttr } from '../observability/genai-attributes.js';
|
|
9
|
+
import { WorkflowDocumentSchema, workflowDepth, workflowGraph } from '../schemas/workflow.js';
|
|
9
10
|
//#endregion
|
|
10
11
|
//#region Constants
|
|
11
12
|
const DEFAULT_MAX_DEPTH = 5;
|
|
@@ -143,8 +144,38 @@ export async function parseAndRunWorkflow(opts) {
|
|
|
143
144
|
executeStep,
|
|
144
145
|
};
|
|
145
146
|
const hydrated = hydrateWorkflow(parseResult.doc, hydrationCtx);
|
|
146
|
-
const
|
|
147
|
-
|
|
147
|
+
const runSpan = beginWorkflowRunSpan(opts.harness, opts.ctx, parseResult.doc);
|
|
148
|
+
try {
|
|
149
|
+
const result = await executeStep(hydrated, opts.input ?? '', opts.ctx);
|
|
150
|
+
return coerceToString(result);
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
runSpan.end();
|
|
154
|
+
await opts.harness.traceExporter.export([
|
|
155
|
+
runSpan,
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Open the root `workflow.run` span and stamp the static DAG onto it (document,
|
|
161
|
+
* version, node/edge graph). The span is installed on `ctx.span` so the LLM
|
|
162
|
+
* step's model/tool spans nest under it — the trace tree then mirrors the
|
|
163
|
+
* declared workflow graph, with the executed path overlaid (issue #50 follow-up).
|
|
164
|
+
*/
|
|
165
|
+
function beginWorkflowRunSpan(harness, ctx, doc) {
|
|
166
|
+
const span = harness.createSpan('workflow.run', null);
|
|
167
|
+
const graph = workflowGraph(doc.root);
|
|
168
|
+
span.setAttribute(NoeticAttr.WORKFLOW_DOCUMENT, JSON.stringify(doc));
|
|
169
|
+
span.setAttribute(NoeticAttr.WORKFLOW_VERSION, doc.version);
|
|
170
|
+
span.setAttribute(NoeticAttr.WORKFLOW_NODE_COUNT, graph.nodes.length);
|
|
171
|
+
span.setAttribute(NoeticAttr.WORKFLOW_NODES, JSON.stringify(graph.nodes));
|
|
172
|
+
span.setAttribute(NoeticAttr.WORKFLOW_EDGES, JSON.stringify(graph.edges));
|
|
173
|
+
installRunSpan(ctx, span);
|
|
174
|
+
return span;
|
|
175
|
+
}
|
|
176
|
+
/** Install the run span as `ctx.span` so descendant model calls parent under it. */
|
|
177
|
+
function installRunSpan(ctx, span) {
|
|
178
|
+
frameworkCast(ctx).span = span;
|
|
148
179
|
}
|
|
149
180
|
function buildToolMap(tools) {
|
|
150
181
|
return new Map(tools.map((t) => [
|
|
@@ -164,6 +164,23 @@ export declare const WorkflowDocumentSchema: z.ZodType<WorkflowDocument>;
|
|
|
164
164
|
export declare function validateWorkflow(input: unknown): WorkflowDocument;
|
|
165
165
|
/** Walks a workflow tree depth-first, yielding each node. */
|
|
166
166
|
export declare function walkWorkflow(node: WorkflowNode): Iterable<WorkflowNode>;
|
|
167
|
+
/** A flattened, JSON-safe view of a workflow tree: its nodes and parent→child edges. */
|
|
168
|
+
export interface WorkflowGraph {
|
|
169
|
+
nodes: Array<{
|
|
170
|
+
id: string;
|
|
171
|
+
kind: WorkflowNode['kind'];
|
|
172
|
+
}>;
|
|
173
|
+
edges: Array<{
|
|
174
|
+
from: string;
|
|
175
|
+
to: string;
|
|
176
|
+
}>;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Flattens a workflow tree into a node + edge list — the static "potential
|
|
180
|
+
* paths" of the DAG, suitable for attaching to a trace span so observers can
|
|
181
|
+
* reconstruct the declared graph independent of which branches actually ran.
|
|
182
|
+
*/
|
|
183
|
+
export declare function workflowGraph(root: WorkflowNode): WorkflowGraph;
|
|
167
184
|
/**
|
|
168
185
|
* Returns the maximum depth of a workflow tree.
|
|
169
186
|
* Leaf nodes (`llm`, `tool`) have depth 0. Structural nodes add +1.
|
package/dist/schemas/workflow.js
CHANGED
|
@@ -279,6 +279,31 @@ export function* walkWorkflow(node) {
|
|
|
279
279
|
yield* walkWorkflow(child);
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* Flattens a workflow tree into a node + edge list — the static "potential
|
|
284
|
+
* paths" of the DAG, suitable for attaching to a trace span so observers can
|
|
285
|
+
* reconstruct the declared graph independent of which branches actually ran.
|
|
286
|
+
*/
|
|
287
|
+
export function workflowGraph(root) {
|
|
288
|
+
const nodes = [];
|
|
289
|
+
const edges = [];
|
|
290
|
+
for (const node of walkWorkflow(root)) {
|
|
291
|
+
nodes.push({
|
|
292
|
+
id: node.id,
|
|
293
|
+
kind: node.kind,
|
|
294
|
+
});
|
|
295
|
+
for (const child of childNodes(node)) {
|
|
296
|
+
edges.push({
|
|
297
|
+
from: node.id,
|
|
298
|
+
to: child.id,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
nodes,
|
|
304
|
+
edges,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
282
307
|
/**
|
|
283
308
|
* Returns the maximum depth of a workflow tree.
|
|
284
309
|
* Leaf nodes (`llm`, `tool`) have depth 0. Structural nodes add +1.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetic-tools/core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
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.3",
|
|
71
71
|
"@openrouter/agent": "^0.6.0",
|
|
72
72
|
"zod": "^4.0.0"
|
|
73
73
|
},
|