@armoriq/sdk-dev 0.6.10 → 0.7.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 +460 -42
- 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 +46 -1
- package/dist/integrations/strands.d.ts.map +1 -1
- package/dist/integrations/strands.js +421 -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 +75 -0
- package/dist/observability/content-capture.d.ts.map +1 -0
- package/dist/observability/content-capture.js +339 -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 +264 -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 +668 -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 +621 -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,17 @@ class ArmorIQStrandsPlugin {
|
|
|
166
185
|
this.parser = args.parser;
|
|
167
186
|
this.eventClasses = args.eventClasses ?? args.factory?.eventClasses;
|
|
168
187
|
}
|
|
188
|
+
frameworkMcpOperation(toolName, itemOrdinal, toolCallId) {
|
|
189
|
+
return {
|
|
190
|
+
category: 'mcp',
|
|
191
|
+
name: 'mcp.execute',
|
|
192
|
+
toolType: 'mcp',
|
|
193
|
+
toolName,
|
|
194
|
+
callId: toolCallId,
|
|
195
|
+
mcpServer: this.parser(toolName).mcp,
|
|
196
|
+
planItemOrdinal: itemOrdinal,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
169
199
|
/** Strands Plugin entrypoint: register our hooks on the agent. */
|
|
170
200
|
async initAgent(agent) {
|
|
171
201
|
if (!this.eventClasses)
|
|
@@ -174,6 +204,10 @@ class ArmorIQStrandsPlugin {
|
|
|
174
204
|
}
|
|
175
205
|
/** Wire the AfterModelCall + BeforeToolCall hooks onto a Strands agent. */
|
|
176
206
|
register(agent) {
|
|
207
|
+
(0, tool_registry_1.registerForClient)(this.factory.client, ...(() => {
|
|
208
|
+
const d = (0, tool_registry_1.declarationsFromStrandsAgent)(agent);
|
|
209
|
+
return [d.tools, d.servers];
|
|
210
|
+
})());
|
|
177
211
|
const add = (agent.addHook ?? agent.hooks?.addCallback)?.bind(agent.addHook ? agent : agent.hooks);
|
|
178
212
|
if (!add) {
|
|
179
213
|
throw new Error('ArmorIQStrands: agent has no addHook/hooks.addCallback');
|
|
@@ -185,14 +219,100 @@ class ArmorIQStrandsPlugin {
|
|
|
185
219
|
add(events.AfterModelCallEvent, (e) => this.onAfterModelCall(e));
|
|
186
220
|
add(events.BeforeToolCallEvent, (e) => this.beforeToolCall(e));
|
|
187
221
|
add(events.AfterToolCallEvent, (e) => this.onAfterToolCall(e));
|
|
222
|
+
// Strands guarantees BeforeModelCallEvent pairs with AfterModelCallEvent,
|
|
223
|
+
// so the model span opens here (before inference) and is only ever ended
|
|
224
|
+
// by onAfterModelCall — including the error path where stopData is
|
|
225
|
+
// absent. Without it (older strands, or a host-injected partial
|
|
226
|
+
// eventClasses map), onAfterModelCall falls back to opening the span
|
|
227
|
+
// itself, same as before this fix.
|
|
228
|
+
if (events.BeforeModelCallEvent) {
|
|
229
|
+
add(events.BeforeModelCallEvent, (e) => this.onBeforeModelCall(e));
|
|
230
|
+
}
|
|
231
|
+
// Strands guarantees this terminal hook runs after both successful and
|
|
232
|
+
// failed invocations. It is the bounded final-flush path when application
|
|
233
|
+
// code disconnects before it can call plugin.close() itself.
|
|
234
|
+
if (events.AfterInvocationEvent) {
|
|
235
|
+
add(events.AfterInvocationEvent, (e) => this.onAfterInvocation(e));
|
|
236
|
+
}
|
|
188
237
|
}
|
|
189
238
|
/** End and ship the request-owned plan session without closing the shared client. */
|
|
190
239
|
async close(status = 'ok') {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.
|
|
194
|
-
this.
|
|
195
|
-
|
|
240
|
+
if (this.closed)
|
|
241
|
+
return;
|
|
242
|
+
this.closed = true;
|
|
243
|
+
await this.closeRequest(undefined, status);
|
|
244
|
+
}
|
|
245
|
+
isClosedInvocation(event) {
|
|
246
|
+
const state = event?.invocationState;
|
|
247
|
+
return (this.closed ||
|
|
248
|
+
(!!state && typeof state === 'object' && this.closedInvocationStates.has(state)));
|
|
249
|
+
}
|
|
250
|
+
requestFor(event) {
|
|
251
|
+
const invocationState = event?.invocationState;
|
|
252
|
+
if (!invocationState ||
|
|
253
|
+
(typeof invocationState !== 'object' && typeof invocationState !== 'function')) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
let request = this.invocationRequests.get(invocationState);
|
|
257
|
+
if (request)
|
|
258
|
+
return request;
|
|
259
|
+
// Keep direct/legacy callers working: existing consumers may preload a
|
|
260
|
+
// session before the first real framework event is delivered.
|
|
261
|
+
if (!this.legacyInvocationState &&
|
|
262
|
+
(this.session || this.planStarted || this.pendingPlanCapture)) {
|
|
263
|
+
this.legacyInvocationState = invocationState;
|
|
264
|
+
request = {
|
|
265
|
+
session: this.session,
|
|
266
|
+
planStarted: this.planStarted,
|
|
267
|
+
pendingPlanCapture: this.pendingPlanCapture,
|
|
268
|
+
cancelledToolUseIds: this.cancelledToolUseIds,
|
|
269
|
+
hadToolError: this.hadToolError,
|
|
270
|
+
pendingTerminalStatus: this.pendingTerminalStatus,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
request = { planStarted: false, cancelledToolUseIds: new Set(), hadToolError: false };
|
|
275
|
+
}
|
|
276
|
+
this.invocationRequests.set(invocationState, request);
|
|
277
|
+
return request;
|
|
278
|
+
}
|
|
279
|
+
async closeRequest(request, status, taskOutcome) {
|
|
280
|
+
// A public `plugin.close()` has no framework event to pass back, but it
|
|
281
|
+
// must still finalize the request state that a prior event adopted as the
|
|
282
|
+
// legacy request. Otherwise its root output is silently discarded.
|
|
283
|
+
const legacyRequest = !request && this.legacyInvocationState !== undefined
|
|
284
|
+
? this.invocationRequests.get(this.legacyInvocationState)
|
|
285
|
+
: undefined;
|
|
286
|
+
const activeRequest = request ?? legacyRequest;
|
|
287
|
+
const session = activeRequest?.session ?? this.session;
|
|
288
|
+
const pendingPlanCapture = activeRequest?.pendingPlanCapture ?? this.pendingPlanCapture;
|
|
289
|
+
const isLegacyRequest = !!activeRequest &&
|
|
290
|
+
this.legacyInvocationState !== undefined &&
|
|
291
|
+
this.invocationRequests.get(this.legacyInvocationState) === activeRequest;
|
|
292
|
+
if (activeRequest) {
|
|
293
|
+
activeRequest.session = undefined;
|
|
294
|
+
activeRequest.planStarted = false;
|
|
295
|
+
activeRequest.cancelledToolUseIds.clear();
|
|
296
|
+
activeRequest.hadToolError = false;
|
|
297
|
+
activeRequest.pendingTerminalStatus = undefined;
|
|
298
|
+
if (isLegacyRequest) {
|
|
299
|
+
this.invocationRequests.delete(this.legacyInvocationState);
|
|
300
|
+
this.legacyInvocationState = undefined;
|
|
301
|
+
this.session = undefined;
|
|
302
|
+
this.planStarted = false;
|
|
303
|
+
this.pendingPlanCapture = undefined;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
this.session = undefined;
|
|
308
|
+
this.planStarted = false;
|
|
309
|
+
this.cancelledToolUseIds.clear();
|
|
310
|
+
this.hadToolError = false;
|
|
311
|
+
this.pendingTerminalStatus = undefined;
|
|
312
|
+
// A caller-owned close wins any in-flight legacy event. Its completion
|
|
313
|
+
// must not resurrect the detached request state.
|
|
314
|
+
this.legacyInvocationState = undefined;
|
|
315
|
+
}
|
|
196
316
|
if (session && pendingPlanCapture && pendingPlanCapture.session === session) {
|
|
197
317
|
try {
|
|
198
318
|
await pendingPlanCapture.promise;
|
|
@@ -201,8 +321,29 @@ class ArmorIQStrandsPlugin {
|
|
|
201
321
|
// Plan capture failures are handled by the lifecycle hook; teardown remains best-effort.
|
|
202
322
|
}
|
|
203
323
|
}
|
|
204
|
-
|
|
205
|
-
|
|
324
|
+
const telemetryState = activeRequest ?? this;
|
|
325
|
+
const error = status === 'ok' ? undefined : new Error('terminal lifecycle');
|
|
326
|
+
const unfinishedToolOutcome = status === 'error' ? 'error' : status === 'timeout' ? 'timeout' :
|
|
327
|
+
status === 'disconnected' ? 'disconnected' : 'cancelled';
|
|
328
|
+
await Promise.all([
|
|
329
|
+
...[...(telemetryState.toolSpans?.values?.() ?? [])].filter(Boolean).map((span) => session?.otelSession?.endTool(span, { outcome: unfinishedToolOutcome, error })),
|
|
330
|
+
...(telemetryState.modelSpan
|
|
331
|
+
? [session?.otelSession?.endModel(telemetryState.modelSpan, {}, error, status)]
|
|
332
|
+
: []),
|
|
333
|
+
]);
|
|
334
|
+
telemetryState.toolSpans?.clear?.();
|
|
335
|
+
telemetryState.modelSpan = undefined;
|
|
336
|
+
telemetryState.invocationFailed = false;
|
|
337
|
+
const rootOutput = telemetryState.finalOutput;
|
|
338
|
+
telemetryState.finalOutput = undefined;
|
|
339
|
+
if (session) {
|
|
340
|
+
if (rootOutput === undefined && taskOutcome === undefined)
|
|
341
|
+
await session.close(status);
|
|
342
|
+
else if (rootOutput === undefined)
|
|
343
|
+
await session.close(status, taskOutcome);
|
|
344
|
+
else
|
|
345
|
+
await session.close(status, taskOutcome, { output: rootOutput });
|
|
346
|
+
}
|
|
206
347
|
}
|
|
207
348
|
emit(kind, payload) {
|
|
208
349
|
if (!this.onEvent)
|
|
@@ -214,7 +355,22 @@ class ArmorIQStrandsPlugin {
|
|
|
214
355
|
console.warn(`[armoriq] onEvent(${kind}) callback failed: ${exc.message}`);
|
|
215
356
|
}
|
|
216
357
|
}
|
|
217
|
-
ensureSession() {
|
|
358
|
+
ensureSession(request) {
|
|
359
|
+
if (request) {
|
|
360
|
+
if (!request.session) {
|
|
361
|
+
request.session = this.scope.startSession({
|
|
362
|
+
mode: this.factory.mode,
|
|
363
|
+
validitySeconds: this.factory.validitySeconds,
|
|
364
|
+
toolNameParser: this.parser,
|
|
365
|
+
defaultMcpName: this.factory.defaultMcpName,
|
|
366
|
+
});
|
|
367
|
+
if (this.legacyInvocationState !== undefined &&
|
|
368
|
+
this.invocationRequests.get(this.legacyInvocationState) === request) {
|
|
369
|
+
this.session = request.session;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return request.session;
|
|
373
|
+
}
|
|
218
374
|
if (!this.session) {
|
|
219
375
|
this.session = this.scope.startSession({
|
|
220
376
|
mode: this.factory.mode,
|
|
@@ -225,19 +381,45 @@ class ArmorIQStrandsPlugin {
|
|
|
225
381
|
}
|
|
226
382
|
return this.session;
|
|
227
383
|
}
|
|
228
|
-
async capturePlan(toolCalls) {
|
|
384
|
+
async capturePlan(toolCalls, request) {
|
|
229
385
|
if (!toolCalls.length)
|
|
230
386
|
return;
|
|
231
|
-
const session = this.ensureSession();
|
|
387
|
+
const session = this.ensureSession(request);
|
|
232
388
|
const pendingPlanCapture = { session, promise: session.startPlan(toolCalls, this.goal) };
|
|
233
|
-
|
|
389
|
+
if (request) {
|
|
390
|
+
request.pendingPlanCapture = pendingPlanCapture;
|
|
391
|
+
if (this.legacyInvocationState !== undefined &&
|
|
392
|
+
this.invocationRequests.get(this.legacyInvocationState) === request) {
|
|
393
|
+
this.pendingPlanCapture = pendingPlanCapture;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else
|
|
397
|
+
this.pendingPlanCapture = pendingPlanCapture;
|
|
234
398
|
try {
|
|
235
399
|
await pendingPlanCapture.promise;
|
|
236
|
-
if (
|
|
400
|
+
if (request) {
|
|
401
|
+
if (request.session === session) {
|
|
402
|
+
request.planStarted = true;
|
|
403
|
+
if (this.legacyInvocationState !== undefined &&
|
|
404
|
+
this.invocationRequests.get(this.legacyInvocationState) === request) {
|
|
405
|
+
this.planStarted = true;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (this.session === session)
|
|
237
410
|
this.planStarted = true;
|
|
238
411
|
}
|
|
239
412
|
finally {
|
|
240
|
-
if (
|
|
413
|
+
if (request) {
|
|
414
|
+
if (request.pendingPlanCapture === pendingPlanCapture) {
|
|
415
|
+
request.pendingPlanCapture = undefined;
|
|
416
|
+
if (this.legacyInvocationState !== undefined &&
|
|
417
|
+
this.invocationRequests.get(this.legacyInvocationState) === request) {
|
|
418
|
+
this.pendingPlanCapture = undefined;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
else if (this.pendingPlanCapture === pendingPlanCapture)
|
|
241
423
|
this.pendingPlanCapture = undefined;
|
|
242
424
|
}
|
|
243
425
|
}
|
|
@@ -257,17 +439,107 @@ class ArmorIQStrandsPlugin {
|
|
|
257
439
|
}
|
|
258
440
|
return calls;
|
|
259
441
|
}
|
|
442
|
+
/**
|
|
443
|
+
* Opens the model span before inference begins, so its recorded duration
|
|
444
|
+
* reflects real model latency rather than the ~0ms window between
|
|
445
|
+
* AfterModelCallEvent firing and this plugin ending the span in the same
|
|
446
|
+
* callback. `onAfterModelCall` always ends whatever span this opened —
|
|
447
|
+
* including when the paired event carries an error and no `stopData`.
|
|
448
|
+
*/
|
|
449
|
+
/** Strands re-reads its tool registry before every model call; mirror it into the inventory. */
|
|
450
|
+
refreshDeclarations(agent) {
|
|
451
|
+
if (!agent)
|
|
452
|
+
return;
|
|
453
|
+
try {
|
|
454
|
+
const d = (0, tool_registry_1.declarationsFromStrandsAgent)(agent);
|
|
455
|
+
(0, tool_registry_1.registerForClient)(this.factory.client, d.tools, d.servers);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
/* inventory problems never stop an agent */
|
|
459
|
+
}
|
|
460
|
+
(0, tool_push_1.noteModelCall)(this.factory.client);
|
|
461
|
+
}
|
|
462
|
+
async onBeforeModelCall(event) {
|
|
463
|
+
if (this.isClosedInvocation(event))
|
|
464
|
+
return;
|
|
465
|
+
this.refreshDeclarations(event?.agent);
|
|
466
|
+
const request = this.requestFor(event);
|
|
467
|
+
const telemetryState = request ?? this;
|
|
468
|
+
try {
|
|
469
|
+
const session = this.ensureSession(request);
|
|
470
|
+
const otel = session.otelSession;
|
|
471
|
+
// Strands does not consistently expose the original prompt in its model
|
|
472
|
+
// hooks. The caller supplied goal is the only stable request input;
|
|
473
|
+
// retain an explicit unavailable marker rather than inventing a prompt.
|
|
474
|
+
const input = this.goal === undefined
|
|
475
|
+
? { availability: 'unavailable', reason: 'framework_goal_not_provided' }
|
|
476
|
+
: this.goal;
|
|
477
|
+
if (typeof otel?.beginRoot === 'function')
|
|
478
|
+
await otel.beginRoot({ input });
|
|
479
|
+
telemetryState.modelSpan = typeof otel?.beginModel === 'function'
|
|
480
|
+
? await otel.beginModel(String(event?.model?.modelId ?? event?.model?.model ?? 'unknown'), { input })
|
|
481
|
+
: undefined;
|
|
482
|
+
}
|
|
483
|
+
catch (exc) {
|
|
484
|
+
console.warn(`[armoriq] strands model span start (before model) failed: ${exc.message}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
260
487
|
async onAfterModelCall(event) {
|
|
488
|
+
if (this.isClosedInvocation(event))
|
|
489
|
+
return;
|
|
490
|
+
const request = this.requestFor(event);
|
|
491
|
+
const telemetryState = request ?? this;
|
|
492
|
+
let modelError = event?.error;
|
|
493
|
+
let session;
|
|
261
494
|
try {
|
|
495
|
+
session = this.ensureSession(request);
|
|
496
|
+
const otel = session.otelSession;
|
|
497
|
+
const input = this.goal === undefined
|
|
498
|
+
? { availability: 'unavailable', reason: 'framework_goal_not_provided' }
|
|
499
|
+
: this.goal;
|
|
500
|
+
// Fallback for hosts whose eventClasses omit BeforeModelCallEvent (older
|
|
501
|
+
// strands, or a host-injected partial map): open the span here so a
|
|
502
|
+
// model call is still captured, even though its duration is then ~0ms.
|
|
503
|
+
if (telemetryState.modelSpan === undefined) {
|
|
504
|
+
if (typeof otel?.beginRoot === 'function')
|
|
505
|
+
await otel.beginRoot({ input });
|
|
506
|
+
telemetryState.modelSpan = typeof otel?.beginModel === 'function'
|
|
507
|
+
? await otel.beginModel(String(event?.model?.modelId ?? event?.model?.model ?? 'unknown'), { input })
|
|
508
|
+
: undefined;
|
|
509
|
+
}
|
|
262
510
|
const stopData = event?.stopData;
|
|
263
|
-
if (!stopData)
|
|
264
|
-
|
|
511
|
+
if (!stopData) {
|
|
512
|
+
// Model errored (or was cancelled) this turn: no message to mint an
|
|
513
|
+
// intent plan from. The AfterInvocationEvent terminal hook carries no
|
|
514
|
+
// error field of its own (see terminalStatus()), so remember this
|
|
515
|
+
// here — it is how the terminal hook learns the invocation failed.
|
|
516
|
+
telemetryState.invocationFailed = true;
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
telemetryState.invocationFailed = false;
|
|
520
|
+
telemetryState.finalOutput = stopData.message;
|
|
265
521
|
const toolCalls = ArmorIQStrandsPlugin.toolCallsFromMessage(stopData?.message);
|
|
266
|
-
|
|
522
|
+
const ordinals = telemetryState.toolOrdinals ?? new Map();
|
|
523
|
+
telemetryState.toolOrdinals = ordinals;
|
|
524
|
+
let ordinal = 0;
|
|
525
|
+
for (const content of stopData?.message?.content ?? []) {
|
|
526
|
+
const toolUse = content?.toolUse ?? content;
|
|
527
|
+
if (typeof toolUse?.toolUseId === 'string') {
|
|
528
|
+
ordinals.set(toolUse.toolUseId, ordinal);
|
|
529
|
+
ordinal += 1;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
await this.capturePlan(toolCalls, request);
|
|
267
533
|
}
|
|
268
534
|
catch (exc) {
|
|
535
|
+
modelError = modelError ?? exc;
|
|
269
536
|
console.warn(`[armoriq] strands plan capture (after model) failed: ${exc.message}`);
|
|
270
537
|
}
|
|
538
|
+
finally {
|
|
539
|
+
const span = telemetryState.modelSpan;
|
|
540
|
+
telemetryState.modelSpan = undefined;
|
|
541
|
+
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 });
|
|
542
|
+
}
|
|
271
543
|
}
|
|
272
544
|
/**
|
|
273
545
|
* Key a tool call so beforeToolCall can tell onAfterToolCall "this one never
|
|
@@ -278,26 +550,63 @@ class ArmorIQStrandsPlugin {
|
|
|
278
550
|
return toolUse?.toolUseId ?? toolUse?.name ?? '';
|
|
279
551
|
}
|
|
280
552
|
/** Cancel the tool and remember that we did, so we never audit it as run. */
|
|
281
|
-
cancelTool(event, toolUse, message) {
|
|
553
|
+
cancelTool(event, toolUse, message, request) {
|
|
282
554
|
event.cancel = message;
|
|
283
555
|
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
284
556
|
if (key)
|
|
285
|
-
this.cancelledToolUseIds.add(key);
|
|
557
|
+
(request?.cancelledToolUseIds ?? this.cancelledToolUseIds).add(key);
|
|
286
558
|
}
|
|
287
559
|
async beforeToolCall(event) {
|
|
560
|
+
if (this.isClosedInvocation(event)) {
|
|
561
|
+
event.cancel = 'ArmorIQ request is closed';
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const request = this.requestFor(event);
|
|
565
|
+
const telemetryState = request ?? this;
|
|
288
566
|
const toolUse = event?.toolUse ?? event?.tool_use ?? {};
|
|
289
567
|
const toolName = toolUse.name ?? '';
|
|
290
568
|
const args = toolUse.input ?? {};
|
|
569
|
+
const itemOrdinal = telemetryState.toolOrdinals?.get(toolUse.toolUseId);
|
|
570
|
+
let policySession;
|
|
571
|
+
let policySpan;
|
|
291
572
|
try {
|
|
292
|
-
if (!this.planStarted) {
|
|
573
|
+
if (!(request?.planStarted ?? this.planStarted)) {
|
|
293
574
|
// Fail-safe: AfterModelCall normally mints the plan first. If it didn't,
|
|
294
575
|
// mint from the tool actually being called — never the agent's full tool set.
|
|
295
|
-
await this.capturePlan([{ name: toolName, args }]);
|
|
576
|
+
await this.capturePlan([{ name: toolName, args }], request);
|
|
577
|
+
}
|
|
578
|
+
const session = this.ensureSession(request);
|
|
579
|
+
policySession = session;
|
|
580
|
+
policySpan = await session.otelSession?.beginPolicy({
|
|
581
|
+
toolName, itemOrdinal, toolCallId: toolUse.toolUseId, arguments: args,
|
|
582
|
+
});
|
|
583
|
+
const decision = await session.check(toolName, args, this.userEmail, { emitOtel: false });
|
|
584
|
+
const policy = decision;
|
|
585
|
+
await session.otelSession?.endPolicy(policySpan ?? { span: null, name: 'armoriq.policy.evaluate' }, {
|
|
586
|
+
decision: decision.allowed ? 'allow' : decision.action === 'hold' ? 'hold' : 'block',
|
|
587
|
+
policyName: decision.matchedPolicy,
|
|
588
|
+
...(policy.policyId ? { policyId: policy.policyId } : {}),
|
|
589
|
+
policyVersion: policy.policyVersion,
|
|
590
|
+
policySource: policy.policySource,
|
|
591
|
+
policyReasonCode: decision.reason,
|
|
592
|
+
defaultAction: policy.defaultAction,
|
|
593
|
+
matchedRuleId: policy.matchedRuleId,
|
|
594
|
+
});
|
|
595
|
+
if (decision.allowed) {
|
|
596
|
+
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
597
|
+
if (key) {
|
|
598
|
+
const toolSpans = telemetryState.toolSpans ?? new Map();
|
|
599
|
+
telemetryState.toolSpans = toolSpans;
|
|
600
|
+
toolSpans.set(key, await session.otelSession?.beginTool({
|
|
601
|
+
toolName,
|
|
602
|
+
itemOrdinal,
|
|
603
|
+
toolCallId: toolUse.toolUseId,
|
|
604
|
+
arguments: args,
|
|
605
|
+
operation: this.frameworkMcpOperation(toolName, itemOrdinal, toolUse.toolUseId),
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
return;
|
|
296
609
|
}
|
|
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
610
|
if (decision.action === 'hold') {
|
|
302
611
|
this.emit('hold', {
|
|
303
612
|
tool: toolName,
|
|
@@ -311,6 +620,18 @@ class ArmorIQStrandsPlugin {
|
|
|
311
620
|
});
|
|
312
621
|
if (outcome === 'approved') {
|
|
313
622
|
this.emit('approved', { tool: toolName, delegationId: decision.delegationId });
|
|
623
|
+
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
624
|
+
if (key) {
|
|
625
|
+
const toolSpans = telemetryState.toolSpans ?? new Map();
|
|
626
|
+
telemetryState.toolSpans = toolSpans;
|
|
627
|
+
toolSpans.set(key, await session.otelSession?.beginTool({
|
|
628
|
+
toolName,
|
|
629
|
+
itemOrdinal,
|
|
630
|
+
toolCallId: toolUse.toolUseId,
|
|
631
|
+
arguments: args,
|
|
632
|
+
operation: this.frameworkMcpOperation(toolName, itemOrdinal, toolUse.toolUseId),
|
|
633
|
+
}));
|
|
634
|
+
}
|
|
314
635
|
return; // approved → do NOT cancel; the tool runs
|
|
315
636
|
}
|
|
316
637
|
this.emit(outcome, {
|
|
@@ -318,19 +639,20 @@ class ArmorIQStrandsPlugin {
|
|
|
318
639
|
delegationId: decision.delegationId,
|
|
319
640
|
reason: decision.reason,
|
|
320
641
|
});
|
|
321
|
-
this.cancelTool(event, toolUse, `ArmorIQ hold ${outcome}: ${decision.reason ?? 'approval required'}
|
|
642
|
+
this.cancelTool(event, toolUse, `ArmorIQ hold ${outcome}: ${decision.reason ?? 'approval required'}`, request);
|
|
322
643
|
return;
|
|
323
644
|
}
|
|
324
645
|
// block (or any other non-allow decision)
|
|
325
646
|
const reason = decision.reason ?? 'blocked by policy';
|
|
326
647
|
this.emit('block', { tool: toolName, reason });
|
|
327
|
-
this.cancelTool(event, toolUse, `ArmorIQ ${decision.action}: ${reason}
|
|
648
|
+
this.cancelTool(event, toolUse, `ArmorIQ ${decision.action}: ${reason}`, request);
|
|
328
649
|
}
|
|
329
650
|
catch (exc) {
|
|
651
|
+
await policySession?.otelSession?.endPolicy(policySpan ?? { span: null, name: 'armoriq.policy.evaluate' }, { error: exc });
|
|
330
652
|
// Fail closed: never let a tool run if enforcement errored — and surface it.
|
|
331
653
|
const msg = exc.message ?? String(exc);
|
|
332
654
|
this.emit('error', { tool: toolName, error: msg });
|
|
333
|
-
this.cancelTool(event, toolUse, `ArmorIQ enforcement error (fail-closed): ${msg}
|
|
655
|
+
this.cancelTool(event, toolUse, `ArmorIQ enforcement error (fail-closed): ${msg}`, request);
|
|
334
656
|
}
|
|
335
657
|
}
|
|
336
658
|
/**
|
|
@@ -346,31 +668,94 @@ class ArmorIQStrandsPlugin {
|
|
|
346
668
|
* allowed to break the run.
|
|
347
669
|
*/
|
|
348
670
|
async onAfterToolCall(event) {
|
|
671
|
+
if (this.isClosedInvocation(event))
|
|
672
|
+
return;
|
|
673
|
+
const request = this.requestFor(event);
|
|
674
|
+
const telemetryState = request ?? this;
|
|
349
675
|
const toolUse = event?.toolUse ?? event?.tool_use ?? {};
|
|
350
676
|
// Consume the mark so a later legitimate call to the same tool is reported.
|
|
351
677
|
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
352
|
-
if (key && this.cancelledToolUseIds.delete(key))
|
|
353
|
-
return;
|
|
354
|
-
|
|
678
|
+
if (key && (request?.cancelledToolUseIds ?? this.cancelledToolUseIds).delete(key))
|
|
679
|
+
return;
|
|
680
|
+
const session = request?.session ?? this.session;
|
|
681
|
+
if (!session || !(request?.planStarted ?? this.planStarted))
|
|
355
682
|
return;
|
|
356
683
|
const toolName = toolUse.name ?? '';
|
|
357
684
|
if (!toolName)
|
|
358
685
|
return;
|
|
359
686
|
const args = toolUse.input ?? {};
|
|
687
|
+
const itemOrdinal = telemetryState.toolOrdinals?.get(toolUse.toolUseId);
|
|
360
688
|
const exc = event?.error;
|
|
689
|
+
if (exc) {
|
|
690
|
+
telemetryState.hadToolError = true;
|
|
691
|
+
telemetryState.pendingTerminalStatus = 'error';
|
|
692
|
+
}
|
|
361
693
|
const status = exc ? 'error' : 'success';
|
|
362
694
|
const errMsg = exc ? (exc.message ?? String(exc)) : undefined;
|
|
363
695
|
const result = exc ? errMsg : event?.result;
|
|
364
696
|
try {
|
|
365
|
-
|
|
697
|
+
const toolSpan = telemetryState.toolSpans?.get(key);
|
|
698
|
+
telemetryState.toolSpans?.delete(key);
|
|
699
|
+
await session.otelSession?.endTool(toolSpan ?? { span: null, name: 'armoriq.tool' }, {
|
|
700
|
+
outcome: exc ? 'error' : 'success', error: exc,
|
|
701
|
+
result,
|
|
702
|
+
});
|
|
703
|
+
await session.report(toolName, args, result, {
|
|
366
704
|
status,
|
|
367
705
|
errorMessage: errMsg,
|
|
706
|
+
emitOtel: false,
|
|
707
|
+
operation: this.frameworkMcpOperation(toolName, itemOrdinal, toolUse.toolUseId),
|
|
368
708
|
});
|
|
369
709
|
}
|
|
370
710
|
catch (reportExc) {
|
|
371
711
|
console.warn(`[armoriq] strands execution report failed for ${toolName}: ${reportExc.message}`);
|
|
372
712
|
}
|
|
373
713
|
}
|
|
714
|
+
async onAfterInvocation(event) {
|
|
715
|
+
const invocationState = event?.invocationState;
|
|
716
|
+
const request = this.requestFor(event);
|
|
717
|
+
const telemetryState = request ?? this;
|
|
718
|
+
const status = request?.pendingTerminalStatus ?? this.terminalStatus(event, telemetryState);
|
|
719
|
+
const taskOutcome = this.taskOutcomeFor(event, request, status);
|
|
720
|
+
await this.closeRequest(request, status, taskOutcome);
|
|
721
|
+
if (invocationState && typeof invocationState === 'object') {
|
|
722
|
+
this.closedInvocationStates.add(invocationState);
|
|
723
|
+
this.invocationRequests.delete(invocationState);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Strands' AfterInvocationEvent carries no `.error` field in the 1.11.2
|
|
728
|
+
* contract — an ordinary invocation failure (the framework's agent loop
|
|
729
|
+
* throwing, most commonly a non-retried model error) is rethrown by the
|
|
730
|
+
* generator right after this terminal hook runs, invisible to the event
|
|
731
|
+
* itself. `onAfterModelCall` tracks that on `telemetryState.invocationFailed`
|
|
732
|
+
* as it observes each model turn, so this only has to consult it here.
|
|
733
|
+
* Native AbortSignal reasons (timeout/disconnect/cancelled) are a more
|
|
734
|
+
* specific, intentional stop and take priority over a generic error.
|
|
735
|
+
*/
|
|
736
|
+
terminalStatus(event, telemetryState) {
|
|
737
|
+
const signal = event?.agent?.cancelSignal;
|
|
738
|
+
if (signal?.aborted) {
|
|
739
|
+
const reason = signal.reason;
|
|
740
|
+
const label = String(reason?.name ?? reason?.code ?? reason ?? '').toLowerCase();
|
|
741
|
+
if (label.includes('timeout'))
|
|
742
|
+
return 'timeout';
|
|
743
|
+
if (label.includes('disconnect') || label.includes('socket') || label.includes('client')) {
|
|
744
|
+
return 'disconnected';
|
|
745
|
+
}
|
|
746
|
+
return 'cancelled';
|
|
747
|
+
}
|
|
748
|
+
if (event?.error || telemetryState?.invocationFailed)
|
|
749
|
+
return 'error';
|
|
750
|
+
return 'ok';
|
|
751
|
+
}
|
|
752
|
+
taskOutcomeFor(event, request, status) {
|
|
753
|
+
const stopReason = event?.result?.stopReason ?? event?.result?.stop_reason;
|
|
754
|
+
const hadToolError = request?.hadToolError ?? this.hadToolError;
|
|
755
|
+
return status === 'error' && hadToolError && stopReason === 'end_turn'
|
|
756
|
+
? 'partially_completed'
|
|
757
|
+
: undefined;
|
|
758
|
+
}
|
|
374
759
|
}
|
|
375
760
|
exports.ArmorIQStrandsPlugin = ArmorIQStrandsPlugin;
|
|
376
761
|
//# sourceMappingURL=strands.js.map
|