@armoriq/sdk-dev 0.6.8 → 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.js +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/cli/index.js +0 -0
- 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 +168 -6
- package/dist/integrations/google_adk.d.ts.map +1 -1
- package/dist/integrations/google_adk.js +797 -103
- package/dist/integrations/google_adk.js.map +1 -1
- package/dist/integrations/langchain.d.ts +51 -1
- package/dist/integrations/langchain.d.ts.map +1 -1
- package/dist/integrations/langchain.js +563 -30
- package/dist/integrations/langchain.js.map +1 -1
- package/dist/integrations/strands.d.ts +48 -0
- package/dist/integrations/strands.d.ts.map +1 -1
- package/dist/integrations/strands.js +441 -27
- 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 -812
- 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 +13 -7
- package/dist/integrations/microsoft_copilot.d.ts +0 -84
- package/dist/integrations/microsoft_copilot.d.ts.map +0 -1
- package/dist/integrations/microsoft_copilot.js +0 -126
- package/dist/integrations/microsoft_copilot.js.map +0 -1
|
@@ -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
|
}
|
|
@@ -151,11 +163,19 @@ class ArmorIQStrandsPlugin {
|
|
|
151
163
|
parser;
|
|
152
164
|
session;
|
|
153
165
|
planStarted = false;
|
|
166
|
+
pendingPlanCapture;
|
|
154
167
|
eventClasses;
|
|
155
168
|
// toolUseIds we cancelled in beforeToolCall. AfterToolCallEvent still fires
|
|
156
169
|
// for a cancelled tool and carries nothing that distinguishes it, so this is
|
|
157
170
|
// the only way to know the tool never actually ran.
|
|
158
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;
|
|
159
179
|
constructor(args) {
|
|
160
180
|
this.factory = args.factory;
|
|
161
181
|
this.scope = args.scope;
|
|
@@ -165,6 +185,17 @@ class ArmorIQStrandsPlugin {
|
|
|
165
185
|
this.parser = args.parser;
|
|
166
186
|
this.eventClasses = args.eventClasses ?? args.factory?.eventClasses;
|
|
167
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
|
+
}
|
|
168
199
|
/** Strands Plugin entrypoint: register our hooks on the agent. */
|
|
169
200
|
async initAgent(agent) {
|
|
170
201
|
if (!this.eventClasses)
|
|
@@ -173,6 +204,10 @@ class ArmorIQStrandsPlugin {
|
|
|
173
204
|
}
|
|
174
205
|
/** Wire the AfterModelCall + BeforeToolCall hooks onto a Strands agent. */
|
|
175
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
|
+
})());
|
|
176
211
|
const add = (agent.addHook ?? agent.hooks?.addCallback)?.bind(agent.addHook ? agent : agent.hooks);
|
|
177
212
|
if (!add) {
|
|
178
213
|
throw new Error('ArmorIQStrands: agent has no addHook/hooks.addCallback');
|
|
@@ -184,6 +219,131 @@ class ArmorIQStrandsPlugin {
|
|
|
184
219
|
add(events.AfterModelCallEvent, (e) => this.onAfterModelCall(e));
|
|
185
220
|
add(events.BeforeToolCallEvent, (e) => this.beforeToolCall(e));
|
|
186
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
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/** End and ship the request-owned plan session without closing the shared client. */
|
|
239
|
+
async close(status = 'ok') {
|
|
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
|
+
}
|
|
316
|
+
if (session && pendingPlanCapture && pendingPlanCapture.session === session) {
|
|
317
|
+
try {
|
|
318
|
+
await pendingPlanCapture.promise;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
// Plan capture failures are handled by the lifecycle hook; teardown remains best-effort.
|
|
322
|
+
}
|
|
323
|
+
}
|
|
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
|
+
}
|
|
187
347
|
}
|
|
188
348
|
emit(kind, payload) {
|
|
189
349
|
if (!this.onEvent)
|
|
@@ -195,7 +355,22 @@ class ArmorIQStrandsPlugin {
|
|
|
195
355
|
console.warn(`[armoriq] onEvent(${kind}) callback failed: ${exc.message}`);
|
|
196
356
|
}
|
|
197
357
|
}
|
|
198
|
-
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
|
+
}
|
|
199
374
|
if (!this.session) {
|
|
200
375
|
this.session = this.scope.startSession({
|
|
201
376
|
mode: this.factory.mode,
|
|
@@ -206,11 +381,47 @@ class ArmorIQStrandsPlugin {
|
|
|
206
381
|
}
|
|
207
382
|
return this.session;
|
|
208
383
|
}
|
|
209
|
-
async capturePlan(toolCalls) {
|
|
384
|
+
async capturePlan(toolCalls, request) {
|
|
210
385
|
if (!toolCalls.length)
|
|
211
386
|
return;
|
|
212
|
-
|
|
213
|
-
this.
|
|
387
|
+
const session = this.ensureSession(request);
|
|
388
|
+
const pendingPlanCapture = { session, promise: session.startPlan(toolCalls, this.goal) };
|
|
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;
|
|
398
|
+
try {
|
|
399
|
+
await pendingPlanCapture.promise;
|
|
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)
|
|
410
|
+
this.planStarted = true;
|
|
411
|
+
}
|
|
412
|
+
finally {
|
|
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)
|
|
423
|
+
this.pendingPlanCapture = undefined;
|
|
424
|
+
}
|
|
214
425
|
}
|
|
215
426
|
/** Extract {name, args} for each toolUse block in a Strands model message. */
|
|
216
427
|
static toolCallsFromMessage(message) {
|
|
@@ -228,17 +439,107 @@ class ArmorIQStrandsPlugin {
|
|
|
228
439
|
}
|
|
229
440
|
return calls;
|
|
230
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
|
+
}
|
|
231
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;
|
|
232
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
|
+
}
|
|
233
510
|
const stopData = event?.stopData;
|
|
234
|
-
if (!stopData)
|
|
235
|
-
|
|
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;
|
|
236
521
|
const toolCalls = ArmorIQStrandsPlugin.toolCallsFromMessage(stopData?.message);
|
|
237
|
-
|
|
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);
|
|
238
533
|
}
|
|
239
534
|
catch (exc) {
|
|
535
|
+
modelError = modelError ?? exc;
|
|
240
536
|
console.warn(`[armoriq] strands plan capture (after model) failed: ${exc.message}`);
|
|
241
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
|
+
}
|
|
242
543
|
}
|
|
243
544
|
/**
|
|
244
545
|
* Key a tool call so beforeToolCall can tell onAfterToolCall "this one never
|
|
@@ -249,26 +550,63 @@ class ArmorIQStrandsPlugin {
|
|
|
249
550
|
return toolUse?.toolUseId ?? toolUse?.name ?? '';
|
|
250
551
|
}
|
|
251
552
|
/** Cancel the tool and remember that we did, so we never audit it as run. */
|
|
252
|
-
cancelTool(event, toolUse, message) {
|
|
553
|
+
cancelTool(event, toolUse, message, request) {
|
|
253
554
|
event.cancel = message;
|
|
254
555
|
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
255
556
|
if (key)
|
|
256
|
-
this.cancelledToolUseIds.add(key);
|
|
557
|
+
(request?.cancelledToolUseIds ?? this.cancelledToolUseIds).add(key);
|
|
257
558
|
}
|
|
258
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;
|
|
259
566
|
const toolUse = event?.toolUse ?? event?.tool_use ?? {};
|
|
260
567
|
const toolName = toolUse.name ?? '';
|
|
261
568
|
const args = toolUse.input ?? {};
|
|
569
|
+
const itemOrdinal = telemetryState.toolOrdinals?.get(toolUse.toolUseId);
|
|
570
|
+
let policySession;
|
|
571
|
+
let policySpan;
|
|
262
572
|
try {
|
|
263
|
-
if (!this.planStarted) {
|
|
573
|
+
if (!(request?.planStarted ?? this.planStarted)) {
|
|
264
574
|
// Fail-safe: AfterModelCall normally mints the plan first. If it didn't,
|
|
265
575
|
// mint from the tool actually being called — never the agent's full tool set.
|
|
266
|
-
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;
|
|
267
609
|
}
|
|
268
|
-
const session = this.ensureSession();
|
|
269
|
-
const decision = await session.check(toolName, args, this.userEmail);
|
|
270
|
-
if (decision.allowed)
|
|
271
|
-
return; // explicit allow → tool runs
|
|
272
610
|
if (decision.action === 'hold') {
|
|
273
611
|
this.emit('hold', {
|
|
274
612
|
tool: toolName,
|
|
@@ -282,6 +620,18 @@ class ArmorIQStrandsPlugin {
|
|
|
282
620
|
});
|
|
283
621
|
if (outcome === 'approved') {
|
|
284
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
|
+
}
|
|
285
635
|
return; // approved → do NOT cancel; the tool runs
|
|
286
636
|
}
|
|
287
637
|
this.emit(outcome, {
|
|
@@ -289,19 +639,20 @@ class ArmorIQStrandsPlugin {
|
|
|
289
639
|
delegationId: decision.delegationId,
|
|
290
640
|
reason: decision.reason,
|
|
291
641
|
});
|
|
292
|
-
this.cancelTool(event, toolUse, `ArmorIQ hold ${outcome}: ${decision.reason ?? 'approval required'}
|
|
642
|
+
this.cancelTool(event, toolUse, `ArmorIQ hold ${outcome}: ${decision.reason ?? 'approval required'}`, request);
|
|
293
643
|
return;
|
|
294
644
|
}
|
|
295
645
|
// block (or any other non-allow decision)
|
|
296
646
|
const reason = decision.reason ?? 'blocked by policy';
|
|
297
647
|
this.emit('block', { tool: toolName, reason });
|
|
298
|
-
this.cancelTool(event, toolUse, `ArmorIQ ${decision.action}: ${reason}
|
|
648
|
+
this.cancelTool(event, toolUse, `ArmorIQ ${decision.action}: ${reason}`, request);
|
|
299
649
|
}
|
|
300
650
|
catch (exc) {
|
|
651
|
+
await policySession?.otelSession?.endPolicy(policySpan ?? { span: null, name: 'armoriq.policy.evaluate' }, { error: exc });
|
|
301
652
|
// Fail closed: never let a tool run if enforcement errored — and surface it.
|
|
302
653
|
const msg = exc.message ?? String(exc);
|
|
303
654
|
this.emit('error', { tool: toolName, error: msg });
|
|
304
|
-
this.cancelTool(event, toolUse, `ArmorIQ enforcement error (fail-closed): ${msg}
|
|
655
|
+
this.cancelTool(event, toolUse, `ArmorIQ enforcement error (fail-closed): ${msg}`, request);
|
|
305
656
|
}
|
|
306
657
|
}
|
|
307
658
|
/**
|
|
@@ -317,31 +668,94 @@ class ArmorIQStrandsPlugin {
|
|
|
317
668
|
* allowed to break the run.
|
|
318
669
|
*/
|
|
319
670
|
async onAfterToolCall(event) {
|
|
671
|
+
if (this.isClosedInvocation(event))
|
|
672
|
+
return;
|
|
673
|
+
const request = this.requestFor(event);
|
|
674
|
+
const telemetryState = request ?? this;
|
|
320
675
|
const toolUse = event?.toolUse ?? event?.tool_use ?? {};
|
|
321
676
|
// Consume the mark so a later legitimate call to the same tool is reported.
|
|
322
677
|
const key = ArmorIQStrandsPlugin.cancelKey(toolUse);
|
|
323
|
-
if (key && this.cancelledToolUseIds.delete(key))
|
|
324
|
-
return;
|
|
325
|
-
|
|
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))
|
|
326
682
|
return;
|
|
327
683
|
const toolName = toolUse.name ?? '';
|
|
328
684
|
if (!toolName)
|
|
329
685
|
return;
|
|
330
686
|
const args = toolUse.input ?? {};
|
|
687
|
+
const itemOrdinal = telemetryState.toolOrdinals?.get(toolUse.toolUseId);
|
|
331
688
|
const exc = event?.error;
|
|
689
|
+
if (exc) {
|
|
690
|
+
telemetryState.hadToolError = true;
|
|
691
|
+
telemetryState.pendingTerminalStatus = 'error';
|
|
692
|
+
}
|
|
332
693
|
const status = exc ? 'error' : 'success';
|
|
333
694
|
const errMsg = exc ? (exc.message ?? String(exc)) : undefined;
|
|
334
695
|
const result = exc ? errMsg : event?.result;
|
|
335
696
|
try {
|
|
336
|
-
|
|
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, {
|
|
337
704
|
status,
|
|
338
705
|
errorMessage: errMsg,
|
|
706
|
+
emitOtel: false,
|
|
707
|
+
operation: this.frameworkMcpOperation(toolName, itemOrdinal, toolUse.toolUseId),
|
|
339
708
|
});
|
|
340
709
|
}
|
|
341
710
|
catch (reportExc) {
|
|
342
711
|
console.warn(`[armoriq] strands execution report failed for ${toolName}: ${reportExc.message}`);
|
|
343
712
|
}
|
|
344
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
|
+
}
|
|
345
759
|
}
|
|
346
760
|
exports.ArmorIQStrandsPlugin = ArmorIQStrandsPlugin;
|
|
347
761
|
//# sourceMappingURL=strands.js.map
|