@noetic-tools/core 1.2.3 → 2.0.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.
- package/dist/harness/agent-harness.d.ts +23 -0
- package/dist/harness/agent-harness.js +49 -12
- package/dist/harness/model-call.js +1 -1
- package/dist/observability/genai-attributes.d.ts +8 -0
- package/dist/observability/genai-attributes.js +8 -0
- package/dist/patterns/dynamic-workflow.js +10 -3
- package/package.json +2 -2
|
@@ -61,6 +61,29 @@ interface AgentHarnessOpts<TParams extends Record<string, unknown> = Record<stri
|
|
|
61
61
|
/** @internal Test-only escape hatch to inject a mock callModel implementation. */
|
|
62
62
|
_testCallModel?: (request: CallModelRequest) => Promise<LLMResponse>;
|
|
63
63
|
}
|
|
64
|
+
/** Env vars consulted when resolving the LLM client (read defensively so the
|
|
65
|
+
* harness also works in runtimes without a `process` global, e.g. the browser). */
|
|
66
|
+
interface LlmEnv {
|
|
67
|
+
noeticApiKey?: string;
|
|
68
|
+
noeticBaseUrl?: string;
|
|
69
|
+
openrouterApiKey?: string;
|
|
70
|
+
}
|
|
71
|
+
/** @public Effective client options resolved from config + environment. */
|
|
72
|
+
export interface ResolvedLlmClient {
|
|
73
|
+
apiKey: string;
|
|
74
|
+
/** Undefined means "use the SDK's default OpenRouter base URL". */
|
|
75
|
+
serverURL?: string;
|
|
76
|
+
cache: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resolve the effective LLM client options from the provider config + environment.
|
|
80
|
+
* Defaults to the Noetic platform (`provider: 'noetic'`): the `'noetic'` provider
|
|
81
|
+
* authenticates with `NOETIC_API_KEY` and targets the Noetic platform base URL,
|
|
82
|
+
* while `'openrouter'` authenticates with `OPENROUTER_API_KEY` and uses the SDK's
|
|
83
|
+
* default OpenRouter endpoint. Returns undefined when no API key is available.
|
|
84
|
+
* Exported for testing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function resolveLlmClient(config: LlmProviderConfig | undefined, env: LlmEnv): ResolvedLlmClient | undefined;
|
|
64
87
|
/**
|
|
65
88
|
* Default agent harness for executing agent steps with built-in channel, memory, and trace support.
|
|
66
89
|
* Provides channel store, memory layer lifecycle, and trace export with no external dependencies.
|
|
@@ -36,23 +36,62 @@ function resolveContextCwdState(rootCwdState, parent, cwdInit) {
|
|
|
36
36
|
}
|
|
37
37
|
return rootCwdState;
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
/** The Noetic platform's OpenAI-Responses-compatible inference base URL. */
|
|
40
|
+
const NOETIC_DEFAULT_BASE_URL = 'https://platform.noetic.tools/v1';
|
|
41
|
+
function readLlmEnv() {
|
|
40
42
|
const processValue = 'process' in globalThis ? globalThis.process : undefined;
|
|
41
|
-
const
|
|
43
|
+
const env = typeof processValue === 'object' && processValue !== null && 'env' in processValue
|
|
42
44
|
? processValue.env
|
|
43
45
|
: undefined;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
46
|
+
if (typeof env !== 'object' || env === null) {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
const str = (value) => typeof value === 'string' ? value : undefined;
|
|
50
|
+
return {
|
|
51
|
+
noeticApiKey: 'NOETIC_API_KEY' in env ? str(env.NOETIC_API_KEY) : undefined,
|
|
52
|
+
noeticBaseUrl: 'NOETIC_BASE_URL' in env ? str(env.NOETIC_BASE_URL) : undefined,
|
|
53
|
+
openrouterApiKey: 'OPENROUTER_API_KEY' in env ? str(env.OPENROUTER_API_KEY) : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve the effective LLM client options from the provider config + environment.
|
|
58
|
+
* Defaults to the Noetic platform (`provider: 'noetic'`): the `'noetic'` provider
|
|
59
|
+
* authenticates with `NOETIC_API_KEY` and targets the Noetic platform base URL,
|
|
60
|
+
* while `'openrouter'` authenticates with `OPENROUTER_API_KEY` and uses the SDK's
|
|
61
|
+
* default OpenRouter endpoint. Returns undefined when no API key is available.
|
|
62
|
+
* Exported for testing.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveLlmClient(config, env) {
|
|
65
|
+
const provider = config?.provider ?? 'noetic';
|
|
66
|
+
const apiKey = config?.apiKey ?? (provider === 'noetic' ? env.noeticApiKey : env.openrouterApiKey);
|
|
48
67
|
if (!apiKey) {
|
|
49
68
|
return undefined;
|
|
50
69
|
}
|
|
51
|
-
|
|
52
|
-
|
|
70
|
+
const serverURL = provider === 'noetic'
|
|
71
|
+
? (config?.baseUrl ?? env.noeticBaseUrl ?? NOETIC_DEFAULT_BASE_URL)
|
|
72
|
+
: config?.baseUrl;
|
|
73
|
+
return {
|
|
74
|
+
apiKey,
|
|
75
|
+
serverURL,
|
|
76
|
+
cache: config?.cache ?? false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createClient(config) {
|
|
80
|
+
const resolved = resolveLlmClient(config, readLlmEnv());
|
|
81
|
+
if (!resolved) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const options = {
|
|
85
|
+
apiKey: resolved.apiKey,
|
|
86
|
+
};
|
|
87
|
+
if (resolved.serverURL) {
|
|
88
|
+
options.serverURL = resolved.serverURL;
|
|
89
|
+
}
|
|
90
|
+
if (resolved.cache) {
|
|
91
|
+
// Inject `X-OpenRouter-Cache: true` on every request so the upstream serves
|
|
53
92
|
// identical model calls from cache without re-billing (deterministic re-runs).
|
|
54
93
|
return new OpenRouter({
|
|
55
|
-
|
|
94
|
+
...options,
|
|
56
95
|
hooks: {
|
|
57
96
|
beforeRequest: (_ctx, request) => {
|
|
58
97
|
request.headers.set('X-OpenRouter-Cache', 'true');
|
|
@@ -61,9 +100,7 @@ function createClient(config) {
|
|
|
61
100
|
},
|
|
62
101
|
});
|
|
63
102
|
}
|
|
64
|
-
return new OpenRouter(
|
|
65
|
-
apiKey,
|
|
66
|
-
});
|
|
103
|
+
return new OpenRouter(options);
|
|
67
104
|
}
|
|
68
105
|
//#region AgentHarness
|
|
69
106
|
/**
|
|
@@ -317,7 +317,7 @@ export class AgentHarnessModelCaller {
|
|
|
317
317
|
throw new NoeticConfigError({
|
|
318
318
|
code: 'NO_LLM_PROVIDER',
|
|
319
319
|
message: 'No LLM provider configured on this harness.',
|
|
320
|
-
hint: 'Pass `llm: { provider: "openrouter", apiKey: "..." }`
|
|
320
|
+
hint: 'Pass `llm: { apiKey: "..." }` (defaults to the Noetic platform) or set NOETIC_API_KEY. For direct OpenRouter, use `llm: { provider: "openrouter", apiKey: "..." }` or set OPENROUTER_API_KEY.',
|
|
321
321
|
});
|
|
322
322
|
}
|
|
323
323
|
async callModel(request) {
|
|
@@ -22,4 +22,12 @@ export declare const NoeticAttr: {
|
|
|
22
22
|
readonly WORKFLOW_EDGES: "noetic.workflow.edges";
|
|
23
23
|
/** Id of the declared workflow node an `llm.call`/`tool.call` span belongs to. */
|
|
24
24
|
readonly NODE_ID: "noetic.node.id";
|
|
25
|
+
/**
|
|
26
|
+
* Conversation/session this run belongs to (the run's `ctx.threadId`). Stamped
|
|
27
|
+
* on the root `workflow.run` span so every turn of a multi-turn session shares
|
|
28
|
+
* one id, letting a consumer group per-run traces back into their session.
|
|
29
|
+
*/
|
|
30
|
+
readonly SESSION_ID: "noetic.session.id";
|
|
31
|
+
/** Resource the session is scoped to (the run's `ctx.resourceId`), when set. */
|
|
32
|
+
readonly RESOURCE_ID: "noetic.resource.id";
|
|
25
33
|
};
|
|
@@ -25,4 +25,12 @@ export const NoeticAttr = {
|
|
|
25
25
|
WORKFLOW_EDGES: 'noetic.workflow.edges',
|
|
26
26
|
/** Id of the declared workflow node an `llm.call`/`tool.call` span belongs to. */
|
|
27
27
|
NODE_ID: 'noetic.node.id',
|
|
28
|
+
/**
|
|
29
|
+
* Conversation/session this run belongs to (the run's `ctx.threadId`). Stamped
|
|
30
|
+
* on the root `workflow.run` span so every turn of a multi-turn session shares
|
|
31
|
+
* one id, letting a consumer group per-run traces back into their session.
|
|
32
|
+
*/
|
|
33
|
+
SESSION_ID: 'noetic.session.id',
|
|
34
|
+
/** Resource the session is scoped to (the run's `ctx.resourceId`), when set. */
|
|
35
|
+
RESOURCE_ID: 'noetic.resource.id',
|
|
28
36
|
};
|
|
@@ -158,9 +158,12 @@ export async function parseAndRunWorkflow(opts) {
|
|
|
158
158
|
}
|
|
159
159
|
/**
|
|
160
160
|
* Open the root `workflow.run` span and stamp the static DAG onto it (document,
|
|
161
|
-
* version, node/edge graph)
|
|
162
|
-
* step's model/tool spans nest under
|
|
163
|
-
* declared workflow graph, with the
|
|
161
|
+
* version, node/edge graph) plus the session/resource the run belongs to. The
|
|
162
|
+
* span is installed on `ctx.span` so the LLM step's model/tool spans nest under
|
|
163
|
+
* it — the trace tree then mirrors the declared workflow graph, with the
|
|
164
|
+
* executed path overlaid (issue #50 follow-up). The session id (`ctx.threadId`)
|
|
165
|
+
* is shared by every turn of a conversation, so consumers can group the per-run
|
|
166
|
+
* traces of one session back together.
|
|
164
167
|
*/
|
|
165
168
|
function beginWorkflowRunSpan(harness, ctx, doc) {
|
|
166
169
|
const span = harness.createSpan('workflow.run', null);
|
|
@@ -170,6 +173,10 @@ function beginWorkflowRunSpan(harness, ctx, doc) {
|
|
|
170
173
|
span.setAttribute(NoeticAttr.WORKFLOW_NODE_COUNT, graph.nodes.length);
|
|
171
174
|
span.setAttribute(NoeticAttr.WORKFLOW_NODES, JSON.stringify(graph.nodes));
|
|
172
175
|
span.setAttribute(NoeticAttr.WORKFLOW_EDGES, JSON.stringify(graph.edges));
|
|
176
|
+
span.setAttribute(NoeticAttr.SESSION_ID, ctx.threadId);
|
|
177
|
+
if (ctx.resourceId !== undefined) {
|
|
178
|
+
span.setAttribute(NoeticAttr.RESOURCE_ID, ctx.resourceId);
|
|
179
|
+
}
|
|
173
180
|
installRunSpan(ctx, span);
|
|
174
181
|
return span;
|
|
175
182
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetic-tools/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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.1.0",
|
|
71
71
|
"@openrouter/agent": "^0.6.0",
|
|
72
72
|
"zod": "^4.0.0"
|
|
73
73
|
},
|