@armoriq/sdk-dev 0.6.10 → 0.8.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/README.md +168 -1
- package/dist/_version.d.ts +1 -1
- package/dist/_version.d.ts.map +1 -1
- package/dist/_version.js +1 -1
- package/dist/_version.js.map +1 -1
- package/dist/cli/commands/auth.d.ts +12 -0
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +522 -49
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/client.d.ts +8 -15
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +20 -18
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +0 -17
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -19
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -14
- package/dist/index.js.map +1 -1
- package/dist/integrations/google_adk.d.ts +155 -7
- package/dist/integrations/google_adk.d.ts.map +1 -1
- package/dist/integrations/google_adk.js +727 -46
- package/dist/integrations/google_adk.js.map +1 -1
- package/dist/integrations/langchain.d.ts +48 -2
- package/dist/integrations/langchain.d.ts.map +1 -1
- package/dist/integrations/langchain.js +528 -33
- package/dist/integrations/langchain.js.map +1 -1
- package/dist/integrations/strands.d.ts +65 -1
- package/dist/integrations/strands.d.ts.map +1 -1
- package/dist/integrations/strands.js +456 -36
- package/dist/integrations/strands.js.map +1 -1
- package/dist/models.d.ts +2 -2
- package/dist/models.d.ts.map +1 -1
- package/dist/observability/content-capture.d.ts +103 -0
- package/dist/observability/content-capture.d.ts.map +1 -0
- package/dist/observability/content-capture.js +423 -0
- package/dist/observability/content-capture.js.map +1 -0
- package/dist/observability/index.d.ts +6 -7
- package/dist/observability/index.d.ts.map +1 -1
- package/dist/observability/index.js +18 -27
- package/dist/observability/index.js.map +1 -1
- package/dist/observability/otel-config.d.ts +47 -0
- package/dist/observability/otel-config.d.ts.map +1 -0
- package/dist/observability/otel-config.js +268 -0
- package/dist/observability/otel-config.js.map +1 -0
- package/dist/observability/otel-export-ceiling.d.ts +96 -0
- package/dist/observability/otel-export-ceiling.d.ts.map +1 -0
- package/dist/observability/otel-export-ceiling.js +271 -0
- package/dist/observability/otel-export-ceiling.js.map +1 -0
- package/dist/observability/otel-runtime.d.ts +103 -0
- package/dist/observability/otel-runtime.d.ts.map +1 -0
- package/dist/observability/otel-runtime.js +680 -0
- package/dist/observability/otel-runtime.js.map +1 -0
- package/dist/observability/otel-session.d.ts +168 -0
- package/dist/observability/otel-session.d.ts.map +1 -0
- package/dist/observability/otel-session.js +630 -0
- package/dist/observability/otel-session.js.map +1 -0
- package/dist/observability/otel-shutdown.d.ts +17 -0
- package/dist/observability/otel-shutdown.d.ts.map +1 -0
- package/dist/observability/otel-shutdown.js +54 -0
- package/dist/observability/otel-shutdown.js.map +1 -0
- package/dist/observability/policy-lease.d.ts +22 -0
- package/dist/observability/policy-lease.d.ts.map +1 -0
- package/dist/observability/policy-lease.js +102 -0
- package/dist/observability/policy-lease.js.map +1 -0
- package/dist/plan_builder.d.ts +5 -4
- package/dist/plan_builder.d.ts.map +1 -1
- package/dist/plan_builder.js +14 -15
- package/dist/plan_builder.js.map +1 -1
- package/dist/session.d.ts +61 -93
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +388 -804
- package/dist/session.js.map +1 -1
- package/dist/token_usage.d.ts +11 -18
- package/dist/token_usage.d.ts.map +1 -1
- package/dist/token_usage.js +29 -94
- package/dist/token_usage.js.map +1 -1
- package/dist/tool_name.d.ts +18 -0
- package/dist/tool_name.d.ts.map +1 -0
- package/dist/tool_name.js +29 -0
- package/dist/tool_name.js.map +1 -0
- package/dist/tool_push.d.ts +28 -0
- package/dist/tool_push.d.ts.map +1 -0
- package/dist/tool_push.js +151 -0
- package/dist/tool_push.js.map +1 -0
- package/dist/tool_registry.d.ts +100 -0
- package/dist/tool_registry.d.ts.map +1 -0
- package/dist/tool_registry.js +440 -0
- package/dist/tool_registry.js.map +1 -0
- package/dist/tool_schema.d.ts +22 -0
- package/dist/tool_schema.d.ts.map +1 -0
- package/dist/tool_schema.js +163 -0
- package/dist/tool_schema.js.map +1 -0
- package/package.json +12 -7
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
*/
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
33
|
exports.ArmorIQStrandsPlugin = exports.ArmorIQStrands = void 0;
|
|
34
|
+
const tool_push_1 = require("../tool_push");
|
|
35
|
+
const tool_registry_1 = require("../tool_registry");
|
|
36
|
+
const tool_name_1 = require("../tool_name");
|
|
34
37
|
const STRANDS_PACKAGE = '@strands-agents/sdk';
|
|
35
38
|
const EVENT_NAMES = [
|
|
36
39
|
'AfterModelCallEvent',
|
|
@@ -50,6 +53,8 @@ function pickEventClasses(mod) {
|
|
|
50
53
|
AfterModelCallEvent: src.AfterModelCallEvent,
|
|
51
54
|
BeforeToolCallEvent: src.BeforeToolCallEvent,
|
|
52
55
|
AfterToolCallEvent: src.AfterToolCallEvent,
|
|
56
|
+
AfterInvocationEvent: typeof src.AfterInvocationEvent === 'function' ? src.AfterInvocationEvent : undefined,
|
|
57
|
+
BeforeModelCallEvent: typeof src.BeforeModelCallEvent === 'function' ? src.BeforeModelCallEvent : undefined,
|
|
53
58
|
};
|
|
54
59
|
}
|
|
55
60
|
/** Load the event classes without a static import, so strands stays optional. */
|
|
@@ -110,16 +115,23 @@ class ArmorIQStrands {
|
|
|
110
115
|
async toolNameParser() {
|
|
111
116
|
if (this.customParser)
|
|
112
117
|
return this.customParser;
|
|
113
|
-
|
|
118
|
+
let toolMap = {};
|
|
119
|
+
try {
|
|
120
|
+
toolMap = (await this.bootstrap()).toolMap ?? {};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// A bootstrap outage must not prevent model-only work. Actual tool
|
|
124
|
+
// execution still passes through the session's fail-closed check.
|
|
125
|
+
console.warn('[armoriq] strands bootstrap unavailable; using safe tool-name fallback');
|
|
126
|
+
}
|
|
114
127
|
const defaultMcp = this.defaultMcpName ?? 'unknown';
|
|
115
128
|
return (toolName) => {
|
|
116
129
|
const mcp = toolMap[toolName];
|
|
117
130
|
if (mcp)
|
|
118
131
|
return { mcp, action: toolName };
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return
|
|
122
|
-
}
|
|
132
|
+
const split = (0, tool_name_1.splitPrefixedToolName)(toolName);
|
|
133
|
+
if (split)
|
|
134
|
+
return split;
|
|
123
135
|
return { mcp: defaultMcp, action: toolName };
|
|
124
136
|
};
|
|
125
137
|
}
|
|
@@ -157,6 +169,13 @@ class ArmorIQStrandsPlugin {
|
|
|
157
169
|
// for a cancelled tool and carries nothing that distinguishes it, so this is
|
|
158
170
|
// the only way to know the tool never actually ran.
|
|
159
171
|
cancelledToolUseIds = new Set();
|
|
172
|
+
hadToolError = false;
|
|
173
|
+
pendingTerminalStatus;
|
|
174
|
+
/** Framework-native request state. A plugin may serve concurrent streams. */
|
|
175
|
+
invocationRequests = new Map();
|
|
176
|
+
closedInvocationStates = new WeakSet();
|
|
177
|
+
closed = false;
|
|
178
|
+
legacyInvocationState;
|
|
160
179
|
constructor(args) {
|
|
161
180
|
this.factory = args.factory;
|
|
162
181
|
this.scope = args.scope;
|
|
@@ -166,6 +185,52 @@ class ArmorIQStrandsPlugin {
|
|
|
166
185
|
this.parser = args.parser;
|
|
167
186
|
this.eventClasses = args.eventClasses ?? args.factory?.eventClasses;
|
|
168
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* `this.parser(toolName)` always returns a `mcp` string, even for a plain
|
|
190
|
+
* tool it never resolved -- the fallback in `ArmorIQStrands.toolNameParser`
|
|
191
|
+
* guesses a placeholder rather than returning nothing:
|
|
192
|
+
* `defaultMcp = this.defaultMcpName ?? 'unknown'`. That placeholder is only
|
|
193
|
+
* ever the literal sentinel `'unknown'` when NO `defaultMcpName` was
|
|
194
|
+
* configured at all -- when one IS configured, the fallback returns that
|
|
195
|
+
* real, customer-chosen value, which is a deliberate decision, not a guess
|
|
196
|
+
* (mirrors the already-shipped Python `_tool_name_parser` semantics: the
|
|
197
|
+
* `_UNKNOWN_MCP` sentinel only applies when `default_mcp_name` is unset).
|
|
198
|
+
* A resolution is therefore real when the name carried a `<mcp>__<action>`
|
|
199
|
+
* prefix (the parser then splits `action` away from the raw `toolName`), or
|
|
200
|
+
* when a `defaultMcpName` is configured, or when the resolved value isn't
|
|
201
|
+
* the unconfigured-fallback's `'unknown'` sentinel. The one known,
|
|
202
|
+
* pre-existing gap (inherited from session.ts's `inventoryFields`) is a
|
|
203
|
+
* genuine toolMap entry that happens to be named `'unknown'` while no
|
|
204
|
+
* `defaultMcpName` is configured -- left as-is, tracked separately.
|
|
205
|
+
*/
|
|
206
|
+
isRealMcpServer(toolName, resolved) {
|
|
207
|
+
if (toolName !== resolved.action)
|
|
208
|
+
return true;
|
|
209
|
+
return !!this.factory.defaultMcpName || resolved.mcp !== 'unknown';
|
|
210
|
+
}
|
|
211
|
+
frameworkToolOperation(toolName, itemOrdinal, toolCallId) {
|
|
212
|
+
const resolved = this.parser(toolName);
|
|
213
|
+
if (this.isRealMcpServer(toolName, resolved)) {
|
|
214
|
+
return {
|
|
215
|
+
category: 'mcp',
|
|
216
|
+
name: 'mcp.execute',
|
|
217
|
+
toolType: 'mcp',
|
|
218
|
+
toolName,
|
|
219
|
+
callId: toolCallId,
|
|
220
|
+
mcpServer: resolved.mcp,
|
|
221
|
+
planItemOrdinal: itemOrdinal,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
category: 'tool',
|
|
226
|
+
name: 'armoriq.tool',
|
|
227
|
+
toolType: 'function',
|
|
228
|
+
toolName,
|
|
229
|
+
callId: toolCallId,
|
|
230
|
+
mcpServer: undefined,
|
|
231
|
+
planItemOrdinal: itemOrdinal,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
169
234
|
/** Strands Plugin entrypoint: register our hooks on the agent. */
|
|
170
235
|
async initAgent(agent) {
|
|
171
236
|
if (!this.eventClasses)
|
|
@@ -174,6 +239,10 @@ class ArmorIQStrandsPlugin {
|
|
|
174
239
|
}
|
|
175
240
|
/** Wire the AfterModelCall + BeforeToolCall hooks onto a Strands agent. */
|
|
176
241
|
register(agent) {
|
|
242
|
+
(0, tool_registry_1.registerForClient)(this.factory.client, ...(() => {
|
|
243
|
+
const d = (0, tool_registry_1.declarationsFromStrandsAgent)(agent);
|
|
244
|
+
return [d.tools, d.servers];
|
|
245
|
+
})());
|
|
177
246
|
const add = (agent.addHook ?? agent.hooks?.addCallback)?.bind(agent.addHook ? agent : agent.hooks);
|
|
178
247
|
if (!add) {
|
|
179
248
|
throw new Error('ArmorIQStrands: agent has no addHook/hooks.addCallback');
|
|
@@ -185,14 +254,100 @@ class ArmorIQStrandsPlugin {
|
|
|
185
254
|
add(events.AfterModelCallEvent, (e) => this.onAfterModelCall(e));
|
|
186
255
|
add(events.BeforeToolCallEvent, (e) => this.beforeToolCall(e));
|
|
187
256
|
add(events.AfterToolCallEvent, (e) => this.onAfterToolCall(e));
|
|
257
|
+
// Strands guarantees BeforeModelCallEvent pairs with AfterModelCallEvent,
|
|
258
|
+
// so the model span opens here (before inference) and is only ever ended
|
|
259
|
+
// by onAfterModelCall — including the error path where stopData is
|
|
260
|
+
// absent. Without it (older strands, or a host-injected partial
|
|
261
|
+
// eventClasses map), onAfterModelCall falls back to opening the span
|
|
262
|
+
// itself, same as before this fix.
|
|
263
|
+
if (events.BeforeModelCallEvent) {
|
|
264
|
+
add(events.BeforeModelCallEvent, (e) => this.onBeforeModelCall(e));
|
|
265
|
+
}
|
|
266
|
+
// Strands guarantees this terminal hook runs after both successful and
|
|
267
|
+
// failed invocations. It is the bounded final-flush path when application
|
|
268
|
+
// code disconnects before it can call plugin.close() itself.
|
|
269
|
+
if (events.AfterInvocationEvent) {
|
|
270
|
+
add(events.AfterInvocationEvent, (e) => this.onAfterInvocation(e));
|
|
271
|
+
}
|
|
188
272
|
}
|
|
189
273
|
/** End and ship the request-owned plan session without closing the shared client. */
|
|
190
274
|
async close(status = 'ok') {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.
|
|
194
|
-
this.
|
|
195
|
-
|
|
275
|
+
if (this.closed)
|
|
276
|
+
return;
|
|
277
|
+
this.closed = true;
|
|
278
|
+
await this.closeRequest(undefined, status);
|
|
279
|
+
}
|
|
280
|
+
isClosedInvocation(event) {
|
|
281
|
+
const state = event?.invocationState;
|
|
282
|
+
return (this.closed ||
|
|
283
|
+
(!!state && typeof state === 'object' && this.closedInvocationStates.has(state)));
|
|
284
|
+
}
|
|
285
|
+
requestFor(event) {
|
|
286
|
+
const invocationState = event?.invocationState;
|
|
287
|
+
if (!invocationState ||
|
|
288
|
+
(typeof invocationState !== 'object' && typeof invocationState !== 'function')) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
let request = this.invocationRequests.get(invocationState);
|
|
292
|
+
if (request)
|
|
293
|
+
return request;
|
|
294
|
+
// Keep direct/legacy callers working: existing consumers may preload a
|
|
295
|
+
// session before the first real framework event is delivered.
|
|
296
|
+
if (!this.legacyInvocationState &&
|
|
297
|
+
(this.session || this.planStarted || this.pendingPlanCapture)) {
|
|
298
|
+
this.legacyInvocationState = invocationState;
|
|
299
|
+
request = {
|
|
300
|
+
session: this.session,
|
|
301
|
+
planStarted: this.planStarted,
|
|
302
|
+
pendingPlanCapture: this.pendingPlanCapture,
|
|
303
|
+
cancelledToolUseIds: this.cancelledToolUseIds,
|
|
304
|
+
hadToolError: this.hadToolError,
|
|
305
|
+
pendingTerminalStatus: this.pendingTerminalStatus,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
request = { planStarted: false, cancelledToolUseIds: new Set(), hadToolError: false };
|
|
310
|
+
}
|
|
311
|
+
this.invocationRequests.set(invocationState, request);
|
|
312
|
+
return request;
|
|
313
|
+
}
|
|
314
|
+
async closeRequest(request, status, taskOutcome) {
|
|
315
|
+
// A public `plugin.close()` has no framework event to pass back, but it
|
|
316
|
+
// must still finalize the request state that a prior event adopted as the
|
|
317
|
+
// legacy request. Otherwise its root output is silently discarded.
|
|
318
|
+
const legacyRequest = !request && this.legacyInvocationState !== undefined
|
|
319
|
+
? this.invocationRequests.get(this.legacyInvocationState)
|
|
320
|
+
: undefined;
|
|
321
|
+
const activeRequest = request ?? legacyRequest;
|
|
322
|
+
const session = activeRequest?.session ?? this.session;
|
|
323
|
+
const pendingPlanCapture = activeRequest?.pendingPlanCapture ?? this.pendingPlanCapture;
|
|
324
|
+
const isLegacyRequest = !!activeRequest &&
|
|
325
|
+
this.legacyInvocationState !== undefined &&
|
|
326
|
+
this.invocationRequests.get(this.legacyInvocationState) === activeRequest;
|
|
327
|
+
if (activeRequest) {
|
|
328
|
+
activeRequest.session = undefined;
|
|
329
|
+
activeRequest.planStarted = false;
|
|
330
|
+
activeRequest.cancelledToolUseIds.clear();
|
|
331
|
+
activeRequest.hadToolError = false;
|
|
332
|
+
activeRequest.pendingTerminalStatus = undefined;
|
|
333
|
+
if (isLegacyRequest) {
|
|
334
|
+
this.invocationRequests.delete(this.legacyInvocationState);
|
|
335
|
+
this.legacyInvocationState = undefined;
|
|
336
|
+
this.session = undefined;
|
|
337
|
+
this.planStarted = false;
|
|
338
|
+
this.pendingPlanCapture = undefined;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
this.session = undefined;
|
|
343
|
+
this.planStarted = false;
|
|
344
|
+
this.cancelledToolUseIds.clear();
|
|
345
|
+
this.hadToolError = false;
|
|
346
|
+
this.pendingTerminalStatus = undefined;
|
|
347
|
+
// A caller-owned close wins any in-flight legacy event. Its completion
|
|
348
|
+
// must not resurrect the detached request state.
|
|
349
|
+
this.legacyInvocationState = undefined;
|
|
350
|
+
}
|
|
196
351
|
if (session && pendingPlanCapture && pendingPlanCapture.session === session) {
|
|
197
352
|
try {
|
|
198
353
|
await pendingPlanCapture.promise;
|
|
@@ -201,8 +356,29 @@ class ArmorIQStrandsPlugin {
|
|
|
201
356
|
// Plan capture failures are handled by the lifecycle hook; teardown remains best-effort.
|
|
202
357
|
}
|
|
203
358
|
}
|
|
204
|
-
|
|
205
|
-
|
|
359
|
+
const telemetryState = activeRequest ?? this;
|
|
360
|
+
const error = status === 'ok' ? undefined : new Error('terminal lifecycle');
|
|
361
|
+
const unfinishedToolOutcome = status === 'error' ? 'error' : status === 'timeout' ? 'timeout' :
|
|
362
|
+
status === 'disconnected' ? 'disconnected' : 'cancelled';
|
|
363
|
+
await Promise.all([
|
|
364
|
+
...[...(telemetryState.toolSpans?.values?.() ?? [])].filter(Boolean).map((span) => session?.otelSession?.endTool(span, { outcome: unfinishedToolOutcome, error })),
|
|
365
|
+
...(telemetryState.modelSpan
|
|
366
|
+
? [session?.otelSession?.endModel(telemetryState.modelSpan, {}, error, status)]
|
|
367
|
+
: []),
|
|
368
|
+
]);
|
|
369
|
+
telemetryState.toolSpans?.clear?.();
|
|
370
|
+
telemetryState.modelSpan = undefined;
|
|
371
|
+
telemetryState.invocationFailed = false;
|
|
372
|
+
const rootOutput = telemetryState.finalOutput;
|
|
373
|
+
telemetryState.finalOutput = undefined;
|
|
374
|
+
if (session) {
|
|
375
|
+
if (rootOutput === undefined && taskOutcome === undefined)
|
|
376
|
+
await session.close(status);
|
|
377
|
+
else if (rootOutput === undefined)
|
|
378
|
+
await session.close(status, taskOutcome);
|
|
379
|
+
else
|
|
380
|
+
await session.close(status, taskOutcome, { output: rootOutput });
|
|
381
|
+
}
|
|
206
382
|
}
|
|
207
383
|
emit(kind, payload) {
|
|
208
384
|
if (!this.onEvent)
|
|
@@ -214,7 +390,22 @@ class ArmorIQStrandsPlugin {
|
|
|
214
390
|
console.warn(`[armoriq] onEvent(${kind}) callback failed: ${exc.message}`);
|
|
215
391
|
}
|
|
216
392
|
}
|
|
217
|
-
ensureSession() {
|
|
393
|
+
ensureSession(request) {
|
|
394
|
+
if (request) {
|
|
395
|
+
if (!request.session) {
|
|
396
|
+
request.session = this.scope.startSession({
|
|
397
|
+
mode: this.factory.mode,
|
|
398
|
+
validitySeconds: this.factory.validitySeconds,
|
|
399
|
+
toolNameParser: this.parser,
|
|
400
|
+
defaultMcpName: this.factory.defaultMcpName,
|
|
401
|
+
});
|
|
402
|
+
if (this.legacyInvocationState !== undefined &&
|
|
403
|
+
this.invocationRequests.get(this.legacyInvocationState) === request) {
|
|
404
|
+
this.session = request.session;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return request.session;
|
|
408
|
+
}
|
|
218
409
|
if (!this.session) {
|
|
219
410
|
this.session = this.scope.startSession({
|
|
220
411
|
mode: this.factory.mode,
|
|
@@ -225,19 +416,45 @@ class ArmorIQStrandsPlugin {
|
|
|
225
416
|
}
|
|
226
417
|
return this.session;
|
|
227
418
|
}
|
|
228
|
-
async capturePlan(toolCalls) {
|
|
419
|
+
async capturePlan(toolCalls, request) {
|
|
229
420
|
if (!toolCalls.length)
|
|
230
421
|
return;
|
|
231
|
-
const session = this.ensureSession();
|
|
422
|
+
const session = this.ensureSession(request);
|
|
232
423
|
const pendingPlanCapture = { session, promise: session.startPlan(toolCalls, this.goal) };
|
|
233
|
-
|
|
424
|
+
if (request) {
|
|
425
|
+
request.pendingPlanCapture = pendingPlanCapture;
|
|
426
|
+
if (this.legacyInvocationState !== undefined &&
|
|
427
|
+
this.invocationRequests.get(this.legacyInvocationState) === request) {
|
|
428
|
+
this.pendingPlanCapture = pendingPlanCapture;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
else
|
|
432
|
+
this.pendingPlanCapture = pendingPlanCapture;
|
|
234
433
|
try {
|
|
235
434
|
await pendingPlanCapture.promise;
|
|
236
|
-
if (
|
|
435
|
+
if (request) {
|
|
436
|
+
if (request.session === session) {
|
|
437
|
+
request.planStarted = true;
|
|
438
|
+
if (this.legacyInvocationState !== undefined &&
|
|
439
|
+
this.invocationRequests.get(this.legacyInvocationState) === request) {
|
|
440
|
+
this.planStarted = true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
else if (this.session === session)
|
|
237
445
|
this.planStarted = true;
|
|
238
446
|
}
|
|
239
447
|
finally {
|
|
240
|
-
if (
|
|
448
|
+
if (request) {
|
|
449
|
+
if (request.pendingPlanCapture === pendingPlanCapture) {
|
|
450
|
+
request.pendingPlanCapture = undefined;
|
|
451
|
+
if (this.legacyInvocationState !== undefined &&
|
|
452
|
+
this.invocationRequests.get(this.legacyInvocationState) === request) {
|
|
453
|
+
this.pendingPlanCapture = undefined;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (this.pendingPlanCapture === pendingPlanCapture)
|
|
241
458
|
this.pendingPlanCapture = undefined;
|
|
242
459
|
}
|
|
243
460
|
}
|
|
@@ -257,17 +474,107 @@ class ArmorIQStrandsPlugin {
|
|
|
257
474
|
}
|
|
258
475
|
return calls;
|
|
259
476
|
}
|
|
477
|
+
/**
|
|
478
|
+
* Opens the model span before inference begins, so its recorded duration
|
|
479
|
+
* reflects real model latency rather than the ~0ms window between
|
|
480
|
+
* AfterModelCallEvent firing and this plugin ending the span in the same
|
|
481
|
+
* callback. `onAfterModelCall` always ends whatever span this opened —
|
|
482
|
+
* including when the paired event carries an error and no `stopData`.
|
|
483
|
+
*/
|
|
484
|
+
/** Strands re-reads its tool registry before every model call; mirror it into the inventory. */
|
|
485
|
+
refreshDeclarations(agent) {
|
|
486
|
+
if (!agent)
|
|
487
|
+
return;
|
|
488
|
+
try {
|
|
489
|
+
const d = (0, tool_registry_1.declarationsFromStrandsAgent)(agent);
|
|
490
|
+
(0, tool_registry_1.registerForClient)(this.factory.client, d.tools, d.servers);
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
/* inventory problems never stop an agent */
|
|
494
|
+
}
|
|
495
|
+
(0, tool_push_1.noteModelCall)(this.factory.client);
|
|
496
|
+
}
|
|
497
|
+
async onBeforeModelCall(event) {
|
|
498
|
+
if (this.isClosedInvocation(event))
|
|
499
|
+
return;
|
|
500
|
+
this.refreshDeclarations(event?.agent);
|
|
501
|
+
const request = this.requestFor(event);
|
|
502
|
+
const telemetryState = request ?? this;
|
|
503
|
+
try {
|
|
504
|
+
const session = this.ensureSession(request);
|
|
505
|
+
const otel = session.otelSession;
|
|
506
|
+
// Strands does not consistently expose the original prompt in its model
|
|
507
|
+
// hooks. The caller supplied goal is the only stable request input;
|
|
508
|
+
// retain an explicit unavailable marker rather than inventing a prompt.
|
|
509
|
+
const input = this.goal === undefined
|
|
510
|
+
? { availability: 'unavailable', reason: 'framework_goal_not_provided' }
|
|
511
|
+
: this.goal;
|
|
512
|
+
if (typeof otel?.beginRoot === 'function')
|
|
513
|
+
await otel.beginRoot({ input });
|
|
514
|
+
telemetryState.modelSpan = typeof otel?.beginModel === 'function'
|
|
515
|
+
? await otel.beginModel(String(event?.model?.modelId ?? event?.model?.model ?? 'unknown'), { input })
|
|
516
|
+
: undefined;
|
|
517
|
+
}
|
|
518
|
+
catch (exc) {
|
|
519
|
+
console.warn(`[armoriq] strands model span start (before model) failed: ${exc.message}`);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
260
522
|
async onAfterModelCall(event) {
|
|
523
|
+
if (this.isClosedInvocation(event))
|
|
524
|
+
return;
|
|
525
|
+
const request = this.requestFor(event);
|
|
526
|
+
const telemetryState = request ?? this;
|
|
527
|
+
let modelError = event?.error;
|
|
528
|
+
let session;
|
|
261
529
|
try {
|
|
530
|
+
session = this.ensureSession(request);
|
|
531
|
+
const otel = session.otelSession;
|
|
532
|
+
const input = this.goal === undefined
|
|
533
|
+
? { availability: 'unavailable', reason: 'framework_goal_not_provided' }
|
|
534
|
+
: this.goal;
|
|
535
|
+
// Fallback for hosts whose eventClasses omit BeforeModelCallEvent (older
|
|
536
|
+
// strands, or a host-injected partial map): open the span here so a
|
|
537
|
+
// model call is still captured, even though its duration is then ~0ms.
|
|
538
|
+
if (telemetryState.modelSpan === undefined) {
|
|
539
|
+
if (typeof otel?.beginRoot === 'function')
|
|
540
|
+
await otel.beginRoot({ input });
|
|
541
|
+
telemetryState.modelSpan = typeof otel?.beginModel === 'function'
|
|
542
|
+
? await otel.beginModel(String(event?.model?.modelId ?? event?.model?.model ?? 'unknown'), { input })
|
|
543
|
+
: undefined;
|
|
544
|
+
}
|
|
262
545
|
const stopData = event?.stopData;
|
|
263
|
-
if (!stopData)
|
|
264
|
-
|
|
546
|
+
if (!stopData) {
|
|
547
|
+
// Model errored (or was cancelled) this turn: no message to mint an
|
|
548
|
+
// intent plan from. The AfterInvocationEvent terminal hook carries no
|
|
549
|
+
// error field of its own (see terminalStatus()), so remember this
|
|
550
|
+
// here — it is how the terminal hook learns the invocation failed.
|
|
551
|
+
telemetryState.invocationFailed = true;
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
telemetryState.invocationFailed = false;
|
|
555
|
+
telemetryState.finalOutput = stopData.message;
|
|
265
556
|
const toolCalls = ArmorIQStrandsPlugin.toolCallsFromMessage(stopData?.message);
|
|
266
|
-
|
|
557
|
+
const ordinals = telemetryState.toolOrdinals ?? new Map();
|
|
558
|
+
telemetryState.toolOrdinals = ordinals;
|
|
559
|
+
let ordinal = 0;
|
|
560
|
+
for (const content of stopData?.message?.content ?? []) {
|
|
561
|
+
const toolUse = content?.toolUse ?? content;
|
|
562
|
+
if (typeof toolUse?.toolUseId === 'string') {
|
|
563
|
+
ordinals.set(toolUse.toolUseId, ordinal);
|
|
564
|
+
ordinal += 1;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
await this.capturePlan(toolCalls, request);
|
|
267
568
|
}
|
|
268
569
|
catch (exc) {
|
|
570
|
+
modelError = modelError ?? exc;
|
|
269
571
|
console.warn(`[armoriq] strands plan capture (after model) failed: ${exc.message}`);
|
|
270
572
|
}
|
|
573
|
+
finally {
|
|
574
|
+
const span = telemetryState.modelSpan;
|
|
575
|
+
telemetryState.modelSpan = undefined;
|
|
576
|
+
await session?.otelSession?.endModel(span ?? { span: null, name: 'gen_ai.chat' }, { ...(event?.stopData?.stopReason ? { 'gen_ai.response.finish_reasons': [event.stopData.stopReason] } : {}) }, modelError, undefined, event?.stopData?.message === undefined ? undefined : { output: event.stopData.message });
|
|
577
|
+
}
|
|
271
578
|
}
|
|
272
579
|
/**
|
|
273
580
|
* Key a tool call so beforeToolCall can tell onAfterToolCall "this one never
|
|
@@ -278,26 +585,63 @@ class ArmorIQStrandsPlugin {
|
|
|
278
585
|
return toolUse?.toolUseId ?? toolUse?.name ?? '';
|
|
279
586
|
}
|
|
280
587
|
/** Cancel the tool and remember that we did, so we never audit it as run. */
|
|
281
|
-
cancelTool(event, toolUse, message) {
|
|
588
|
+
cancelTool(event, toolUse, message, request) {
|
|
282
589
|
event.cancel = message;
|
|
283
590
|
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
284
591
|
if (key)
|
|
285
|
-
this.cancelledToolUseIds.add(key);
|
|
592
|
+
(request?.cancelledToolUseIds ?? this.cancelledToolUseIds).add(key);
|
|
286
593
|
}
|
|
287
594
|
async beforeToolCall(event) {
|
|
595
|
+
if (this.isClosedInvocation(event)) {
|
|
596
|
+
event.cancel = 'ArmorIQ request is closed';
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const request = this.requestFor(event);
|
|
600
|
+
const telemetryState = request ?? this;
|
|
288
601
|
const toolUse = event?.toolUse ?? event?.tool_use ?? {};
|
|
289
602
|
const toolName = toolUse.name ?? '';
|
|
290
603
|
const args = toolUse.input ?? {};
|
|
604
|
+
const itemOrdinal = telemetryState.toolOrdinals?.get(toolUse.toolUseId);
|
|
605
|
+
let policySession;
|
|
606
|
+
let policySpan;
|
|
291
607
|
try {
|
|
292
|
-
if (!this.planStarted) {
|
|
608
|
+
if (!(request?.planStarted ?? this.planStarted)) {
|
|
293
609
|
// Fail-safe: AfterModelCall normally mints the plan first. If it didn't,
|
|
294
610
|
// mint from the tool actually being called — never the agent's full tool set.
|
|
295
|
-
await this.capturePlan([{ name: toolName, args }]);
|
|
611
|
+
await this.capturePlan([{ name: toolName, args }], request);
|
|
612
|
+
}
|
|
613
|
+
const session = this.ensureSession(request);
|
|
614
|
+
policySession = session;
|
|
615
|
+
policySpan = await session.otelSession?.beginPolicy({
|
|
616
|
+
toolName, itemOrdinal, toolCallId: toolUse.toolUseId, arguments: args,
|
|
617
|
+
});
|
|
618
|
+
const decision = await session.check(toolName, args, this.userEmail, { emitOtel: false });
|
|
619
|
+
const policy = decision;
|
|
620
|
+
await session.otelSession?.endPolicy(policySpan ?? { span: null, name: 'armoriq.policy.evaluate' }, {
|
|
621
|
+
decision: decision.allowed ? 'allow' : decision.action === 'hold' ? 'hold' : 'block',
|
|
622
|
+
policyName: decision.matchedPolicy,
|
|
623
|
+
...(policy.policyId ? { policyId: policy.policyId } : {}),
|
|
624
|
+
policyVersion: policy.policyVersion,
|
|
625
|
+
policySource: policy.policySource,
|
|
626
|
+
policyReasonCode: decision.reason,
|
|
627
|
+
defaultAction: policy.defaultAction,
|
|
628
|
+
matchedRuleId: policy.matchedRuleId,
|
|
629
|
+
});
|
|
630
|
+
if (decision.allowed) {
|
|
631
|
+
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
632
|
+
if (key) {
|
|
633
|
+
const toolSpans = telemetryState.toolSpans ?? new Map();
|
|
634
|
+
telemetryState.toolSpans = toolSpans;
|
|
635
|
+
toolSpans.set(key, await session.otelSession?.beginTool({
|
|
636
|
+
toolName,
|
|
637
|
+
itemOrdinal,
|
|
638
|
+
toolCallId: toolUse.toolUseId,
|
|
639
|
+
arguments: args,
|
|
640
|
+
operation: this.frameworkToolOperation(toolName, itemOrdinal, toolUse.toolUseId),
|
|
641
|
+
}));
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
296
644
|
}
|
|
297
|
-
const session = this.ensureSession();
|
|
298
|
-
const decision = await session.check(toolName, args, this.userEmail);
|
|
299
|
-
if (decision.allowed)
|
|
300
|
-
return; // explicit allow → tool runs
|
|
301
645
|
if (decision.action === 'hold') {
|
|
302
646
|
this.emit('hold', {
|
|
303
647
|
tool: toolName,
|
|
@@ -311,6 +655,18 @@ class ArmorIQStrandsPlugin {
|
|
|
311
655
|
});
|
|
312
656
|
if (outcome === 'approved') {
|
|
313
657
|
this.emit('approved', { tool: toolName, delegationId: decision.delegationId });
|
|
658
|
+
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
659
|
+
if (key) {
|
|
660
|
+
const toolSpans = telemetryState.toolSpans ?? new Map();
|
|
661
|
+
telemetryState.toolSpans = toolSpans;
|
|
662
|
+
toolSpans.set(key, await session.otelSession?.beginTool({
|
|
663
|
+
toolName,
|
|
664
|
+
itemOrdinal,
|
|
665
|
+
toolCallId: toolUse.toolUseId,
|
|
666
|
+
arguments: args,
|
|
667
|
+
operation: this.frameworkToolOperation(toolName, itemOrdinal, toolUse.toolUseId),
|
|
668
|
+
}));
|
|
669
|
+
}
|
|
314
670
|
return; // approved → do NOT cancel; the tool runs
|
|
315
671
|
}
|
|
316
672
|
this.emit(outcome, {
|
|
@@ -318,19 +674,20 @@ class ArmorIQStrandsPlugin {
|
|
|
318
674
|
delegationId: decision.delegationId,
|
|
319
675
|
reason: decision.reason,
|
|
320
676
|
});
|
|
321
|
-
this.cancelTool(event, toolUse, `ArmorIQ hold ${outcome}: ${decision.reason ?? 'approval required'}
|
|
677
|
+
this.cancelTool(event, toolUse, `ArmorIQ hold ${outcome}: ${decision.reason ?? 'approval required'}`, request);
|
|
322
678
|
return;
|
|
323
679
|
}
|
|
324
680
|
// block (or any other non-allow decision)
|
|
325
681
|
const reason = decision.reason ?? 'blocked by policy';
|
|
326
682
|
this.emit('block', { tool: toolName, reason });
|
|
327
|
-
this.cancelTool(event, toolUse, `ArmorIQ ${decision.action}: ${reason}
|
|
683
|
+
this.cancelTool(event, toolUse, `ArmorIQ ${decision.action}: ${reason}`, request);
|
|
328
684
|
}
|
|
329
685
|
catch (exc) {
|
|
686
|
+
await policySession?.otelSession?.endPolicy(policySpan ?? { span: null, name: 'armoriq.policy.evaluate' }, { error: exc });
|
|
330
687
|
// Fail closed: never let a tool run if enforcement errored — and surface it.
|
|
331
688
|
const msg = exc.message ?? String(exc);
|
|
332
689
|
this.emit('error', { tool: toolName, error: msg });
|
|
333
|
-
this.cancelTool(event, toolUse, `ArmorIQ enforcement error (fail-closed): ${msg}
|
|
690
|
+
this.cancelTool(event, toolUse, `ArmorIQ enforcement error (fail-closed): ${msg}`, request);
|
|
334
691
|
}
|
|
335
692
|
}
|
|
336
693
|
/**
|
|
@@ -346,31 +703,94 @@ class ArmorIQStrandsPlugin {
|
|
|
346
703
|
* allowed to break the run.
|
|
347
704
|
*/
|
|
348
705
|
async onAfterToolCall(event) {
|
|
706
|
+
if (this.isClosedInvocation(event))
|
|
707
|
+
return;
|
|
708
|
+
const request = this.requestFor(event);
|
|
709
|
+
const telemetryState = request ?? this;
|
|
349
710
|
const toolUse = event?.toolUse ?? event?.tool_use ?? {};
|
|
350
711
|
// Consume the mark so a later legitimate call to the same tool is reported.
|
|
351
712
|
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
352
|
-
if (key && this.cancelledToolUseIds.delete(key))
|
|
353
|
-
return;
|
|
354
|
-
|
|
713
|
+
if (key && (request?.cancelledToolUseIds ?? this.cancelledToolUseIds).delete(key))
|
|
714
|
+
return;
|
|
715
|
+
const session = request?.session ?? this.session;
|
|
716
|
+
if (!session || !(request?.planStarted ?? this.planStarted))
|
|
355
717
|
return;
|
|
356
718
|
const toolName = toolUse.name ?? '';
|
|
357
719
|
if (!toolName)
|
|
358
720
|
return;
|
|
359
721
|
const args = toolUse.input ?? {};
|
|
722
|
+
const itemOrdinal = telemetryState.toolOrdinals?.get(toolUse.toolUseId);
|
|
360
723
|
const exc = event?.error;
|
|
724
|
+
if (exc) {
|
|
725
|
+
telemetryState.hadToolError = true;
|
|
726
|
+
telemetryState.pendingTerminalStatus = 'error';
|
|
727
|
+
}
|
|
361
728
|
const status = exc ? 'error' : 'success';
|
|
362
729
|
const errMsg = exc ? (exc.message ?? String(exc)) : undefined;
|
|
363
730
|
const result = exc ? errMsg : event?.result;
|
|
364
731
|
try {
|
|
365
|
-
|
|
732
|
+
const toolSpan = telemetryState.toolSpans?.get(key);
|
|
733
|
+
telemetryState.toolSpans?.delete(key);
|
|
734
|
+
await session.otelSession?.endTool(toolSpan ?? { span: null, name: 'armoriq.tool' }, {
|
|
735
|
+
outcome: exc ? 'error' : 'success', error: exc,
|
|
736
|
+
result,
|
|
737
|
+
});
|
|
738
|
+
await session.report(toolName, args, result, {
|
|
366
739
|
status,
|
|
367
740
|
errorMessage: errMsg,
|
|
741
|
+
emitOtel: false,
|
|
742
|
+
operation: this.frameworkToolOperation(toolName, itemOrdinal, toolUse.toolUseId),
|
|
368
743
|
});
|
|
369
744
|
}
|
|
370
745
|
catch (reportExc) {
|
|
371
746
|
console.warn(`[armoriq] strands execution report failed for ${toolName}: ${reportExc.message}`);
|
|
372
747
|
}
|
|
373
748
|
}
|
|
749
|
+
async onAfterInvocation(event) {
|
|
750
|
+
const invocationState = event?.invocationState;
|
|
751
|
+
const request = this.requestFor(event);
|
|
752
|
+
const telemetryState = request ?? this;
|
|
753
|
+
const status = request?.pendingTerminalStatus ?? this.terminalStatus(event, telemetryState);
|
|
754
|
+
const taskOutcome = this.taskOutcomeFor(event, request, status);
|
|
755
|
+
await this.closeRequest(request, status, taskOutcome);
|
|
756
|
+
if (invocationState && typeof invocationState === 'object') {
|
|
757
|
+
this.closedInvocationStates.add(invocationState);
|
|
758
|
+
this.invocationRequests.delete(invocationState);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Strands' AfterInvocationEvent carries no `.error` field in the 1.11.2
|
|
763
|
+
* contract — an ordinary invocation failure (the framework's agent loop
|
|
764
|
+
* throwing, most commonly a non-retried model error) is rethrown by the
|
|
765
|
+
* generator right after this terminal hook runs, invisible to the event
|
|
766
|
+
* itself. `onAfterModelCall` tracks that on `telemetryState.invocationFailed`
|
|
767
|
+
* as it observes each model turn, so this only has to consult it here.
|
|
768
|
+
* Native AbortSignal reasons (timeout/disconnect/cancelled) are a more
|
|
769
|
+
* specific, intentional stop and take priority over a generic error.
|
|
770
|
+
*/
|
|
771
|
+
terminalStatus(event, telemetryState) {
|
|
772
|
+
const signal = event?.agent?.cancelSignal;
|
|
773
|
+
if (signal?.aborted) {
|
|
774
|
+
const reason = signal.reason;
|
|
775
|
+
const label = String(reason?.name ?? reason?.code ?? reason ?? '').toLowerCase();
|
|
776
|
+
if (label.includes('timeout'))
|
|
777
|
+
return 'timeout';
|
|
778
|
+
if (label.includes('disconnect') || label.includes('socket') || label.includes('client')) {
|
|
779
|
+
return 'disconnected';
|
|
780
|
+
}
|
|
781
|
+
return 'cancelled';
|
|
782
|
+
}
|
|
783
|
+
if (event?.error || telemetryState?.invocationFailed)
|
|
784
|
+
return 'error';
|
|
785
|
+
return 'ok';
|
|
786
|
+
}
|
|
787
|
+
taskOutcomeFor(event, request, status) {
|
|
788
|
+
const stopReason = event?.result?.stopReason ?? event?.result?.stop_reason;
|
|
789
|
+
const hadToolError = request?.hadToolError ?? this.hadToolError;
|
|
790
|
+
return status === 'error' && hadToolError && stopReason === 'end_turn'
|
|
791
|
+
? 'partially_completed'
|
|
792
|
+
: undefined;
|
|
793
|
+
}
|
|
374
794
|
}
|
|
375
795
|
exports.ArmorIQStrandsPlugin = ArmorIQStrandsPlugin;
|
|
376
796
|
//# sourceMappingURL=strands.js.map
|