@contractspec/lib.ai-agent 2.9.1 → 3.1.1
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 +14 -2
- package/dist/agent/agent-factory.d.ts +3 -0
- package/dist/agent/agent-factory.js +209 -27
- package/dist/agent/contract-spec-agent.d.ts +3 -0
- package/dist/agent/contract-spec-agent.js +207 -26
- package/dist/agent/index.js +218 -29
- package/dist/agent/json-runner.d.ts +3 -0
- package/dist/agent/json-runner.js +214 -28
- package/dist/agent/unified-agent.d.ts +3 -0
- package/dist/agent/unified-agent.js +211 -27
- package/dist/exporters/claude-agent-exporter.js +5 -0
- package/dist/exporters/index.js +5 -0
- package/dist/exporters/opencode-exporter.js +5 -0
- package/dist/index.js +5 -0
- package/dist/interop/index.d.ts +1 -0
- package/dist/interop/index.js +5 -0
- package/dist/interop/runtime-adapters.d.ts +36 -0
- package/dist/interop/runtime-adapters.js +1 -0
- package/dist/interop/spec-consumer.js +5 -0
- package/dist/node/agent/agent-factory.js +209 -27
- package/dist/node/agent/contract-spec-agent.js +207 -26
- package/dist/node/agent/index.js +218 -29
- package/dist/node/agent/json-runner.js +214 -28
- package/dist/node/agent/unified-agent.js +211 -27
- package/dist/node/exporters/claude-agent-exporter.js +5 -0
- package/dist/node/exporters/index.js +5 -0
- package/dist/node/exporters/opencode-exporter.js +5 -0
- package/dist/node/index.js +5 -0
- package/dist/node/interop/index.js +5 -0
- package/dist/node/interop/runtime-adapters.js +0 -0
- package/dist/node/interop/spec-consumer.js +5 -0
- package/dist/node/providers/claude-agent-sdk/adapter.js +5 -0
- package/dist/node/providers/claude-agent-sdk/index.js +5 -0
- package/dist/node/providers/index.js +5 -0
- package/dist/node/providers/opencode-sdk/adapter.js +5 -0
- package/dist/node/providers/opencode-sdk/index.js +5 -0
- package/dist/node/spec/index.js +5 -0
- package/dist/node/spec/spec.js +5 -0
- package/dist/node/tools/index.js +86 -10
- package/dist/node/tools/tool-adapter.js +86 -10
- package/dist/providers/claude-agent-sdk/adapter.js +5 -0
- package/dist/providers/claude-agent-sdk/index.js +5 -0
- package/dist/providers/index.js +5 -0
- package/dist/providers/opencode-sdk/adapter.js +5 -0
- package/dist/providers/opencode-sdk/index.js +5 -0
- package/dist/spec/index.js +5 -0
- package/dist/spec/spec.d.ts +27 -0
- package/dist/spec/spec.js +5 -0
- package/dist/spec/spec.test.d.ts +1 -0
- package/dist/tools/index.js +86 -10
- package/dist/tools/mcp-client.d.ts +6 -0
- package/dist/tools/mcp-server.d.ts +4 -0
- package/dist/tools/tool-adapter.js +86 -10
- package/dist/tools/tool-adapter.test.d.ts +1 -0
- package/dist/types.d.ts +15 -0
- package/package.json +27 -15
|
@@ -2142,6 +2142,11 @@ function defineAgent(spec) {
|
|
|
2142
2142
|
if (!spec.tools?.length) {
|
|
2143
2143
|
throw new Error(i18n.t("error.agentRequiresTool", { key: spec.meta.key }));
|
|
2144
2144
|
}
|
|
2145
|
+
for (const [portName, portRef] of Object.entries(spec.runtime?.ports ?? {})) {
|
|
2146
|
+
if (portRef !== undefined && portRef.trim().length === 0) {
|
|
2147
|
+
throw new Error(`Agent ${spec.meta.key} has invalid runtime config: port "${portName}" must not be empty`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2145
2150
|
const toolNames = new Set;
|
|
2146
2151
|
for (const tool of spec.tools) {
|
|
2147
2152
|
if (toolNames.has(tool.name)) {
|
|
@@ -2297,21 +2302,39 @@ var init_json_schema_to_zod = () => {};
|
|
|
2297
2302
|
// src/tools/tool-adapter.ts
|
|
2298
2303
|
import { tool } from "ai";
|
|
2299
2304
|
function specToolToAISDKTool(specTool, handler, context = {}) {
|
|
2305
|
+
let lastInvocationAt;
|
|
2300
2306
|
return tool({
|
|
2301
2307
|
description: specTool.description ?? specTool.name,
|
|
2302
2308
|
inputSchema: jsonSchemaToZodSafe(specTool.schema),
|
|
2303
2309
|
needsApproval: specTool.requiresApproval ?? !specTool.automationSafe,
|
|
2304
2310
|
execute: async (input) => {
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2311
|
+
const now = Date.now();
|
|
2312
|
+
const cooldownMs = normalizeDuration(specTool.cooldownMs);
|
|
2313
|
+
if (cooldownMs && lastInvocationAt !== undefined) {
|
|
2314
|
+
const elapsed = now - lastInvocationAt;
|
|
2315
|
+
if (elapsed < cooldownMs) {
|
|
2316
|
+
const retryAfterMs = cooldownMs - elapsed;
|
|
2317
|
+
throw createToolExecutionError(`Tool "${specTool.name}" is cooling down. Retry in ${retryAfterMs}ms.`, "TOOL_COOLDOWN_ACTIVE", retryAfterMs);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
const timeoutMs = normalizeDuration(specTool.timeoutMs);
|
|
2321
|
+
const { signal, dispose } = createTimeoutSignal(context.signal, timeoutMs);
|
|
2322
|
+
try {
|
|
2323
|
+
const execution = handler(input, {
|
|
2324
|
+
agentId: context.agentId ?? "unknown",
|
|
2325
|
+
sessionId: context.sessionId ?? "unknown",
|
|
2326
|
+
tenantId: context.tenantId,
|
|
2327
|
+
actorId: context.actorId,
|
|
2328
|
+
locale: context.locale,
|
|
2329
|
+
metadata: context.metadata,
|
|
2330
|
+
signal
|
|
2331
|
+
});
|
|
2332
|
+
const result = timeoutMs ? await withTimeout(execution, timeoutMs, specTool.name) : await execution;
|
|
2333
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
2334
|
+
} finally {
|
|
2335
|
+
dispose();
|
|
2336
|
+
lastInvocationAt = Date.now();
|
|
2337
|
+
}
|
|
2315
2338
|
}
|
|
2316
2339
|
});
|
|
2317
2340
|
}
|
|
@@ -2336,6 +2359,64 @@ function createToolHandler(handler) {
|
|
|
2336
2359
|
function buildToolHandlers(handlersObj) {
|
|
2337
2360
|
return new Map(Object.entries(handlersObj));
|
|
2338
2361
|
}
|
|
2362
|
+
function normalizeDuration(value) {
|
|
2363
|
+
if (value === undefined) {
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
if (!Number.isFinite(value)) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (value <= 0) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
return Math.round(value);
|
|
2373
|
+
}
|
|
2374
|
+
function withTimeout(execution, timeoutMs, toolName) {
|
|
2375
|
+
return new Promise((resolve, reject) => {
|
|
2376
|
+
const timeoutHandle = setTimeout(() => {
|
|
2377
|
+
reject(createToolExecutionError(`Tool "${toolName}" timed out after ${timeoutMs}ms.`, "TOOL_EXECUTION_TIMEOUT"));
|
|
2378
|
+
}, timeoutMs);
|
|
2379
|
+
execution.then((result) => {
|
|
2380
|
+
clearTimeout(timeoutHandle);
|
|
2381
|
+
resolve(result);
|
|
2382
|
+
}).catch((error) => {
|
|
2383
|
+
clearTimeout(timeoutHandle);
|
|
2384
|
+
reject(error);
|
|
2385
|
+
});
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
function createTimeoutSignal(signal, timeoutMs) {
|
|
2389
|
+
const controller = new AbortController;
|
|
2390
|
+
const abortFromSource = () => controller.abort();
|
|
2391
|
+
if (signal) {
|
|
2392
|
+
if (signal.aborted) {
|
|
2393
|
+
controller.abort();
|
|
2394
|
+
} else {
|
|
2395
|
+
signal.addEventListener("abort", abortFromSource);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
const timeoutHandle = timeoutMs !== undefined ? setTimeout(() => {
|
|
2399
|
+
controller.abort();
|
|
2400
|
+
}, timeoutMs) : undefined;
|
|
2401
|
+
return {
|
|
2402
|
+
signal: controller.signal,
|
|
2403
|
+
dispose: () => {
|
|
2404
|
+
if (timeoutHandle !== undefined) {
|
|
2405
|
+
clearTimeout(timeoutHandle);
|
|
2406
|
+
}
|
|
2407
|
+
if (signal) {
|
|
2408
|
+
signal.removeEventListener("abort", abortFromSource);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
function createToolExecutionError(message, code, retryAfterMs) {
|
|
2414
|
+
return Object.assign(new Error(message), {
|
|
2415
|
+
code,
|
|
2416
|
+
kind: "retryable",
|
|
2417
|
+
retryAfterMs
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2339
2420
|
var init_tool_adapter = __esm(() => {
|
|
2340
2421
|
init_json_schema_to_zod();
|
|
2341
2422
|
init_i18n();
|
|
@@ -3151,7 +3232,13 @@ class ContractSpecAgent {
|
|
|
3151
3232
|
steps: [],
|
|
3152
3233
|
metadata: params.options?.metadata
|
|
3153
3234
|
});
|
|
3235
|
+
} else if (existing.status !== "running") {
|
|
3236
|
+
await this.config.sessionStore.update(sessionId, { status: "running" });
|
|
3154
3237
|
}
|
|
3238
|
+
await this.config.sessionStore.appendMessage(sessionId, {
|
|
3239
|
+
role: "user",
|
|
3240
|
+
content: params.prompt
|
|
3241
|
+
});
|
|
3155
3242
|
}
|
|
3156
3243
|
const prompt = params.systemOverride ? `${this.instructions}
|
|
3157
3244
|
|
|
@@ -3163,24 +3250,41 @@ ${params.prompt}` : params.prompt;
|
|
|
3163
3250
|
traceId,
|
|
3164
3251
|
options: params.options
|
|
3165
3252
|
});
|
|
3166
|
-
const
|
|
3167
|
-
const
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3253
|
+
const effectiveMaxSteps = resolveMaxSteps(params.maxSteps, this.spec.maxSteps);
|
|
3254
|
+
const inner = this.createInnerAgent(model, effectiveMaxSteps);
|
|
3255
|
+
let result;
|
|
3256
|
+
try {
|
|
3257
|
+
result = await inner.generate({
|
|
3258
|
+
prompt,
|
|
3259
|
+
abortSignal: params.signal,
|
|
3260
|
+
options: {
|
|
3261
|
+
tenantId: params.options?.tenantId,
|
|
3262
|
+
actorId: params.options?.actorId,
|
|
3263
|
+
sessionId,
|
|
3264
|
+
metadata: params.options?.metadata
|
|
3265
|
+
}
|
|
3266
|
+
});
|
|
3267
|
+
} catch (error) {
|
|
3268
|
+
if (this.config.sessionStore) {
|
|
3269
|
+
await this.config.sessionStore.update(sessionId, {
|
|
3270
|
+
status: "failed"
|
|
3271
|
+
});
|
|
3175
3272
|
}
|
|
3176
|
-
}).finally(() => {
|
|
3177
3273
|
this.activeStepContexts.delete(sessionId);
|
|
3178
|
-
|
|
3274
|
+
throw error;
|
|
3275
|
+
}
|
|
3276
|
+
this.activeStepContexts.delete(sessionId);
|
|
3277
|
+
const escalationError = resolveEscalationError(this.spec, result.finishReason);
|
|
3179
3278
|
if (this.config.sessionStore) {
|
|
3279
|
+
await this.config.sessionStore.appendMessage(sessionId, {
|
|
3280
|
+
role: "assistant",
|
|
3281
|
+
content: result.text
|
|
3282
|
+
});
|
|
3180
3283
|
await this.config.sessionStore.update(sessionId, {
|
|
3181
|
-
status: "completed"
|
|
3284
|
+
status: escalationError ? "escalated" : "completed"
|
|
3182
3285
|
});
|
|
3183
3286
|
}
|
|
3287
|
+
const session = this.config.sessionStore ? await this.config.sessionStore.get(sessionId) : null;
|
|
3184
3288
|
return {
|
|
3185
3289
|
text: result.text,
|
|
3186
3290
|
steps: result.steps,
|
|
@@ -3197,7 +3301,16 @@ ${params.prompt}` : params.prompt;
|
|
|
3197
3301
|
output: tr.output
|
|
3198
3302
|
})),
|
|
3199
3303
|
finishReason: result.finishReason,
|
|
3200
|
-
usage: result.usage
|
|
3304
|
+
usage: result.usage,
|
|
3305
|
+
session: session ?? undefined,
|
|
3306
|
+
pendingApproval: escalationError ? {
|
|
3307
|
+
toolName: this.spec.policy?.escalation?.approvalWorkflow ?? "approval_required",
|
|
3308
|
+
toolCallId: `approval_${sessionId}`,
|
|
3309
|
+
args: {
|
|
3310
|
+
reason: escalationError.message,
|
|
3311
|
+
code: escalationError.code
|
|
3312
|
+
}
|
|
3313
|
+
} : undefined
|
|
3201
3314
|
};
|
|
3202
3315
|
}
|
|
3203
3316
|
async stream(params) {
|
|
@@ -3220,7 +3333,28 @@ ${params.prompt}` : params.prompt;
|
|
|
3220
3333
|
traceId,
|
|
3221
3334
|
options: params.options
|
|
3222
3335
|
});
|
|
3223
|
-
const
|
|
3336
|
+
const effectiveMaxSteps = resolveMaxSteps(params.maxSteps, this.spec.maxSteps);
|
|
3337
|
+
const inner = this.createInnerAgent(model, effectiveMaxSteps);
|
|
3338
|
+
if (this.config.sessionStore) {
|
|
3339
|
+
const existing = await this.config.sessionStore.get(sessionId);
|
|
3340
|
+
if (!existing) {
|
|
3341
|
+
await this.config.sessionStore.create({
|
|
3342
|
+
sessionId,
|
|
3343
|
+
agentId: this.id,
|
|
3344
|
+
tenantId: params.options?.tenantId,
|
|
3345
|
+
actorId: params.options?.actorId,
|
|
3346
|
+
status: "running",
|
|
3347
|
+
messages: [],
|
|
3348
|
+
steps: [],
|
|
3349
|
+
metadata: params.options?.metadata
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
await this.config.sessionStore.appendMessage(sessionId, {
|
|
3353
|
+
role: "user",
|
|
3354
|
+
content: params.prompt
|
|
3355
|
+
});
|
|
3356
|
+
await this.config.sessionStore.update(sessionId, { status: "running" });
|
|
3357
|
+
}
|
|
3224
3358
|
return inner.stream({
|
|
3225
3359
|
prompt,
|
|
3226
3360
|
abortSignal: params.signal,
|
|
@@ -3236,6 +3370,9 @@ ${params.prompt}` : params.prompt;
|
|
|
3236
3370
|
const sessionId = step.options?.sessionId;
|
|
3237
3371
|
if (sessionId && this.config.sessionStore) {
|
|
3238
3372
|
await this.config.sessionStore.appendStep(sessionId, step);
|
|
3373
|
+
await this.config.sessionStore.update(sessionId, {
|
|
3374
|
+
status: step.finishReason === "tool-calls" ? "waiting" : "running"
|
|
3375
|
+
});
|
|
3239
3376
|
}
|
|
3240
3377
|
if (this.config.telemetryCollector) {
|
|
3241
3378
|
const now = new Date;
|
|
@@ -3259,12 +3396,12 @@ ${params.prompt}` : params.prompt;
|
|
|
3259
3396
|
}
|
|
3260
3397
|
}
|
|
3261
3398
|
}
|
|
3262
|
-
createInnerAgent(model) {
|
|
3399
|
+
createInnerAgent(model, maxSteps) {
|
|
3263
3400
|
return new ToolLoopAgent({
|
|
3264
3401
|
model,
|
|
3265
3402
|
instructions: this.instructions,
|
|
3266
3403
|
tools: this.tools,
|
|
3267
|
-
stopWhen: stepCountIs(
|
|
3404
|
+
stopWhen: stepCountIs(maxSteps),
|
|
3268
3405
|
callOptionsSchema: ContractSpecCallOptionsSchema,
|
|
3269
3406
|
onStepFinish: async (step) => {
|
|
3270
3407
|
await this.handleStepFinish(step);
|
|
@@ -3272,6 +3409,10 @@ ${params.prompt}` : params.prompt;
|
|
|
3272
3409
|
});
|
|
3273
3410
|
}
|
|
3274
3411
|
async resolveModelForCall(params) {
|
|
3412
|
+
if (this.config.modelSelector && params.options?.selectionContext) {
|
|
3413
|
+
const { model } = await this.config.modelSelector.selectAndCreate(params.options.selectionContext);
|
|
3414
|
+
return model;
|
|
3415
|
+
}
|
|
3275
3416
|
const posthogConfig = this.config.posthogConfig;
|
|
3276
3417
|
if (!posthogConfig) {
|
|
3277
3418
|
return this.config.model;
|
|
@@ -3296,6 +3437,46 @@ ${params.prompt}` : params.prompt;
|
|
|
3296
3437
|
return createPostHogTracedModel2(this.config.model, posthogConfig, tracingOptions);
|
|
3297
3438
|
}
|
|
3298
3439
|
}
|
|
3440
|
+
function resolveMaxSteps(overrideMaxSteps, specMaxSteps) {
|
|
3441
|
+
const candidate = overrideMaxSteps ?? specMaxSteps ?? 10;
|
|
3442
|
+
if (!Number.isFinite(candidate)) {
|
|
3443
|
+
return 10;
|
|
3444
|
+
}
|
|
3445
|
+
if (candidate < 1) {
|
|
3446
|
+
return 1;
|
|
3447
|
+
}
|
|
3448
|
+
return Math.round(candidate);
|
|
3449
|
+
}
|
|
3450
|
+
function resolveEscalationError(spec, finishReason) {
|
|
3451
|
+
const escalation = spec.policy?.escalation;
|
|
3452
|
+
if (!escalation) {
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
if (escalation.onTimeout && finishReason === "length") {
|
|
3456
|
+
return {
|
|
3457
|
+
kind: "timeout",
|
|
3458
|
+
code: "AGENT_TIMEOUT_ESCALATION",
|
|
3459
|
+
message: "Agent reached max step budget and requires escalation."
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
if (escalation.onToolFailure && finishReason === "error") {
|
|
3463
|
+
return {
|
|
3464
|
+
kind: "retryable",
|
|
3465
|
+
code: "AGENT_TOOL_FAILURE_ESCALATION",
|
|
3466
|
+
message: "Agent encountered a tool failure and requires escalation."
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
const confidenceThreshold = escalation.confidenceThreshold;
|
|
3470
|
+
const defaultConfidence = spec.policy?.confidence?.default;
|
|
3471
|
+
if (confidenceThreshold !== undefined && defaultConfidence !== undefined && defaultConfidence < confidenceThreshold) {
|
|
3472
|
+
return {
|
|
3473
|
+
kind: "policy_blocked",
|
|
3474
|
+
code: "AGENT_CONFIDENCE_ESCALATION",
|
|
3475
|
+
message: `Agent default confidence (${defaultConfidence}) is below escalation threshold (${confidenceThreshold}).`
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3299
3480
|
var ContractSpecCallOptionsSchema;
|
|
3300
3481
|
var init_contract_spec_agent = __esm(() => {
|
|
3301
3482
|
init_spec();
|
|
@@ -3343,7 +3524,11 @@ function getDefaultSpec(locale) {
|
|
|
3343
3524
|
tools: []
|
|
3344
3525
|
};
|
|
3345
3526
|
}
|
|
3346
|
-
function resolveModel(options) {
|
|
3527
|
+
async function resolveModel(options) {
|
|
3528
|
+
if (options.modelSelector && options.selectionContext) {
|
|
3529
|
+
const { model } = await options.modelSelector.selectAndCreate(options.selectionContext);
|
|
3530
|
+
return model;
|
|
3531
|
+
}
|
|
3347
3532
|
if (options.model)
|
|
3348
3533
|
return options.model;
|
|
3349
3534
|
if (options.provider) {
|
|
@@ -3376,7 +3561,8 @@ function ensureToolHandlers(spec, handlers, locale) {
|
|
|
3376
3561
|
}
|
|
3377
3562
|
}
|
|
3378
3563
|
async function createAgentJsonRunner(options) {
|
|
3379
|
-
const
|
|
3564
|
+
const resolved = await resolveModel(options);
|
|
3565
|
+
const model = applyModelSettings(resolved, {
|
|
3380
3566
|
temperature: options.temperature ?? 0
|
|
3381
3567
|
});
|
|
3382
3568
|
const baseSpec = options.spec ?? getDefaultSpec(options.locale);
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import type { LanguageModel } from 'ai';
|
|
29
29
|
import type { ProviderConfig } from '@contractspec/lib.ai-providers/types';
|
|
30
|
+
import type { ModelSelector, ModelSelectionContext } from '@contractspec/lib.ai-providers/selector-types';
|
|
30
31
|
import type { AgentSpec } from '../spec/spec';
|
|
31
32
|
import type { McpClientConfig } from '../tools/mcp-client';
|
|
32
33
|
import type { AgentCallOptions, AgentGenerateResult, ToolHandler } from '../types';
|
|
@@ -42,6 +43,8 @@ export interface UnifiedAgentBackendConfig {
|
|
|
42
43
|
temperature?: number;
|
|
43
44
|
maxTokens?: number;
|
|
44
45
|
mcpServers?: McpClientConfig[];
|
|
46
|
+
modelSelector?: ModelSelector;
|
|
47
|
+
selectionContext?: ModelSelectionContext;
|
|
45
48
|
};
|
|
46
49
|
'claude-agent-sdk'?: ClaudeAgentSDKConfig;
|
|
47
50
|
'opencode-sdk'?: OpenCodeSDKConfig;
|
|
@@ -2142,6 +2142,11 @@ function defineAgent(spec) {
|
|
|
2142
2142
|
if (!spec.tools?.length) {
|
|
2143
2143
|
throw new Error(i18n.t("error.agentRequiresTool", { key: spec.meta.key }));
|
|
2144
2144
|
}
|
|
2145
|
+
for (const [portName, portRef] of Object.entries(spec.runtime?.ports ?? {})) {
|
|
2146
|
+
if (portRef !== undefined && portRef.trim().length === 0) {
|
|
2147
|
+
throw new Error(`Agent ${spec.meta.key} has invalid runtime config: port "${portName}" must not be empty`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2145
2150
|
const toolNames = new Set;
|
|
2146
2151
|
for (const tool of spec.tools) {
|
|
2147
2152
|
if (toolNames.has(tool.name)) {
|
|
@@ -2297,21 +2302,39 @@ var init_json_schema_to_zod = () => {};
|
|
|
2297
2302
|
// src/tools/tool-adapter.ts
|
|
2298
2303
|
import { tool } from "ai";
|
|
2299
2304
|
function specToolToAISDKTool(specTool, handler, context = {}) {
|
|
2305
|
+
let lastInvocationAt;
|
|
2300
2306
|
return tool({
|
|
2301
2307
|
description: specTool.description ?? specTool.name,
|
|
2302
2308
|
inputSchema: jsonSchemaToZodSafe(specTool.schema),
|
|
2303
2309
|
needsApproval: specTool.requiresApproval ?? !specTool.automationSafe,
|
|
2304
2310
|
execute: async (input) => {
|
|
2305
|
-
const
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2311
|
+
const now = Date.now();
|
|
2312
|
+
const cooldownMs = normalizeDuration(specTool.cooldownMs);
|
|
2313
|
+
if (cooldownMs && lastInvocationAt !== undefined) {
|
|
2314
|
+
const elapsed = now - lastInvocationAt;
|
|
2315
|
+
if (elapsed < cooldownMs) {
|
|
2316
|
+
const retryAfterMs = cooldownMs - elapsed;
|
|
2317
|
+
throw createToolExecutionError(`Tool "${specTool.name}" is cooling down. Retry in ${retryAfterMs}ms.`, "TOOL_COOLDOWN_ACTIVE", retryAfterMs);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
const timeoutMs = normalizeDuration(specTool.timeoutMs);
|
|
2321
|
+
const { signal, dispose } = createTimeoutSignal(context.signal, timeoutMs);
|
|
2322
|
+
try {
|
|
2323
|
+
const execution = handler(input, {
|
|
2324
|
+
agentId: context.agentId ?? "unknown",
|
|
2325
|
+
sessionId: context.sessionId ?? "unknown",
|
|
2326
|
+
tenantId: context.tenantId,
|
|
2327
|
+
actorId: context.actorId,
|
|
2328
|
+
locale: context.locale,
|
|
2329
|
+
metadata: context.metadata,
|
|
2330
|
+
signal
|
|
2331
|
+
});
|
|
2332
|
+
const result = timeoutMs ? await withTimeout(execution, timeoutMs, specTool.name) : await execution;
|
|
2333
|
+
return typeof result === "string" ? result : JSON.stringify(result);
|
|
2334
|
+
} finally {
|
|
2335
|
+
dispose();
|
|
2336
|
+
lastInvocationAt = Date.now();
|
|
2337
|
+
}
|
|
2315
2338
|
}
|
|
2316
2339
|
});
|
|
2317
2340
|
}
|
|
@@ -2336,6 +2359,64 @@ function createToolHandler(handler) {
|
|
|
2336
2359
|
function buildToolHandlers(handlersObj) {
|
|
2337
2360
|
return new Map(Object.entries(handlersObj));
|
|
2338
2361
|
}
|
|
2362
|
+
function normalizeDuration(value) {
|
|
2363
|
+
if (value === undefined) {
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
if (!Number.isFinite(value)) {
|
|
2367
|
+
return;
|
|
2368
|
+
}
|
|
2369
|
+
if (value <= 0) {
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
return Math.round(value);
|
|
2373
|
+
}
|
|
2374
|
+
function withTimeout(execution, timeoutMs, toolName) {
|
|
2375
|
+
return new Promise((resolve, reject) => {
|
|
2376
|
+
const timeoutHandle = setTimeout(() => {
|
|
2377
|
+
reject(createToolExecutionError(`Tool "${toolName}" timed out after ${timeoutMs}ms.`, "TOOL_EXECUTION_TIMEOUT"));
|
|
2378
|
+
}, timeoutMs);
|
|
2379
|
+
execution.then((result) => {
|
|
2380
|
+
clearTimeout(timeoutHandle);
|
|
2381
|
+
resolve(result);
|
|
2382
|
+
}).catch((error) => {
|
|
2383
|
+
clearTimeout(timeoutHandle);
|
|
2384
|
+
reject(error);
|
|
2385
|
+
});
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
function createTimeoutSignal(signal, timeoutMs) {
|
|
2389
|
+
const controller = new AbortController;
|
|
2390
|
+
const abortFromSource = () => controller.abort();
|
|
2391
|
+
if (signal) {
|
|
2392
|
+
if (signal.aborted) {
|
|
2393
|
+
controller.abort();
|
|
2394
|
+
} else {
|
|
2395
|
+
signal.addEventListener("abort", abortFromSource);
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
const timeoutHandle = timeoutMs !== undefined ? setTimeout(() => {
|
|
2399
|
+
controller.abort();
|
|
2400
|
+
}, timeoutMs) : undefined;
|
|
2401
|
+
return {
|
|
2402
|
+
signal: controller.signal,
|
|
2403
|
+
dispose: () => {
|
|
2404
|
+
if (timeoutHandle !== undefined) {
|
|
2405
|
+
clearTimeout(timeoutHandle);
|
|
2406
|
+
}
|
|
2407
|
+
if (signal) {
|
|
2408
|
+
signal.removeEventListener("abort", abortFromSource);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
function createToolExecutionError(message, code, retryAfterMs) {
|
|
2414
|
+
return Object.assign(new Error(message), {
|
|
2415
|
+
code,
|
|
2416
|
+
kind: "retryable",
|
|
2417
|
+
retryAfterMs
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2339
2420
|
var init_tool_adapter = __esm(() => {
|
|
2340
2421
|
init_json_schema_to_zod();
|
|
2341
2422
|
init_i18n();
|
|
@@ -3151,7 +3232,13 @@ class ContractSpecAgent {
|
|
|
3151
3232
|
steps: [],
|
|
3152
3233
|
metadata: params.options?.metadata
|
|
3153
3234
|
});
|
|
3235
|
+
} else if (existing.status !== "running") {
|
|
3236
|
+
await this.config.sessionStore.update(sessionId, { status: "running" });
|
|
3154
3237
|
}
|
|
3238
|
+
await this.config.sessionStore.appendMessage(sessionId, {
|
|
3239
|
+
role: "user",
|
|
3240
|
+
content: params.prompt
|
|
3241
|
+
});
|
|
3155
3242
|
}
|
|
3156
3243
|
const prompt = params.systemOverride ? `${this.instructions}
|
|
3157
3244
|
|
|
@@ -3163,24 +3250,41 @@ ${params.prompt}` : params.prompt;
|
|
|
3163
3250
|
traceId,
|
|
3164
3251
|
options: params.options
|
|
3165
3252
|
});
|
|
3166
|
-
const
|
|
3167
|
-
const
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3253
|
+
const effectiveMaxSteps = resolveMaxSteps(params.maxSteps, this.spec.maxSteps);
|
|
3254
|
+
const inner = this.createInnerAgent(model, effectiveMaxSteps);
|
|
3255
|
+
let result;
|
|
3256
|
+
try {
|
|
3257
|
+
result = await inner.generate({
|
|
3258
|
+
prompt,
|
|
3259
|
+
abortSignal: params.signal,
|
|
3260
|
+
options: {
|
|
3261
|
+
tenantId: params.options?.tenantId,
|
|
3262
|
+
actorId: params.options?.actorId,
|
|
3263
|
+
sessionId,
|
|
3264
|
+
metadata: params.options?.metadata
|
|
3265
|
+
}
|
|
3266
|
+
});
|
|
3267
|
+
} catch (error) {
|
|
3268
|
+
if (this.config.sessionStore) {
|
|
3269
|
+
await this.config.sessionStore.update(sessionId, {
|
|
3270
|
+
status: "failed"
|
|
3271
|
+
});
|
|
3175
3272
|
}
|
|
3176
|
-
}).finally(() => {
|
|
3177
3273
|
this.activeStepContexts.delete(sessionId);
|
|
3178
|
-
|
|
3274
|
+
throw error;
|
|
3275
|
+
}
|
|
3276
|
+
this.activeStepContexts.delete(sessionId);
|
|
3277
|
+
const escalationError = resolveEscalationError(this.spec, result.finishReason);
|
|
3179
3278
|
if (this.config.sessionStore) {
|
|
3279
|
+
await this.config.sessionStore.appendMessage(sessionId, {
|
|
3280
|
+
role: "assistant",
|
|
3281
|
+
content: result.text
|
|
3282
|
+
});
|
|
3180
3283
|
await this.config.sessionStore.update(sessionId, {
|
|
3181
|
-
status: "completed"
|
|
3284
|
+
status: escalationError ? "escalated" : "completed"
|
|
3182
3285
|
});
|
|
3183
3286
|
}
|
|
3287
|
+
const session = this.config.sessionStore ? await this.config.sessionStore.get(sessionId) : null;
|
|
3184
3288
|
return {
|
|
3185
3289
|
text: result.text,
|
|
3186
3290
|
steps: result.steps,
|
|
@@ -3197,7 +3301,16 @@ ${params.prompt}` : params.prompt;
|
|
|
3197
3301
|
output: tr.output
|
|
3198
3302
|
})),
|
|
3199
3303
|
finishReason: result.finishReason,
|
|
3200
|
-
usage: result.usage
|
|
3304
|
+
usage: result.usage,
|
|
3305
|
+
session: session ?? undefined,
|
|
3306
|
+
pendingApproval: escalationError ? {
|
|
3307
|
+
toolName: this.spec.policy?.escalation?.approvalWorkflow ?? "approval_required",
|
|
3308
|
+
toolCallId: `approval_${sessionId}`,
|
|
3309
|
+
args: {
|
|
3310
|
+
reason: escalationError.message,
|
|
3311
|
+
code: escalationError.code
|
|
3312
|
+
}
|
|
3313
|
+
} : undefined
|
|
3201
3314
|
};
|
|
3202
3315
|
}
|
|
3203
3316
|
async stream(params) {
|
|
@@ -3220,7 +3333,28 @@ ${params.prompt}` : params.prompt;
|
|
|
3220
3333
|
traceId,
|
|
3221
3334
|
options: params.options
|
|
3222
3335
|
});
|
|
3223
|
-
const
|
|
3336
|
+
const effectiveMaxSteps = resolveMaxSteps(params.maxSteps, this.spec.maxSteps);
|
|
3337
|
+
const inner = this.createInnerAgent(model, effectiveMaxSteps);
|
|
3338
|
+
if (this.config.sessionStore) {
|
|
3339
|
+
const existing = await this.config.sessionStore.get(sessionId);
|
|
3340
|
+
if (!existing) {
|
|
3341
|
+
await this.config.sessionStore.create({
|
|
3342
|
+
sessionId,
|
|
3343
|
+
agentId: this.id,
|
|
3344
|
+
tenantId: params.options?.tenantId,
|
|
3345
|
+
actorId: params.options?.actorId,
|
|
3346
|
+
status: "running",
|
|
3347
|
+
messages: [],
|
|
3348
|
+
steps: [],
|
|
3349
|
+
metadata: params.options?.metadata
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
await this.config.sessionStore.appendMessage(sessionId, {
|
|
3353
|
+
role: "user",
|
|
3354
|
+
content: params.prompt
|
|
3355
|
+
});
|
|
3356
|
+
await this.config.sessionStore.update(sessionId, { status: "running" });
|
|
3357
|
+
}
|
|
3224
3358
|
return inner.stream({
|
|
3225
3359
|
prompt,
|
|
3226
3360
|
abortSignal: params.signal,
|
|
@@ -3236,6 +3370,9 @@ ${params.prompt}` : params.prompt;
|
|
|
3236
3370
|
const sessionId = step.options?.sessionId;
|
|
3237
3371
|
if (sessionId && this.config.sessionStore) {
|
|
3238
3372
|
await this.config.sessionStore.appendStep(sessionId, step);
|
|
3373
|
+
await this.config.sessionStore.update(sessionId, {
|
|
3374
|
+
status: step.finishReason === "tool-calls" ? "waiting" : "running"
|
|
3375
|
+
});
|
|
3239
3376
|
}
|
|
3240
3377
|
if (this.config.telemetryCollector) {
|
|
3241
3378
|
const now = new Date;
|
|
@@ -3259,12 +3396,12 @@ ${params.prompt}` : params.prompt;
|
|
|
3259
3396
|
}
|
|
3260
3397
|
}
|
|
3261
3398
|
}
|
|
3262
|
-
createInnerAgent(model) {
|
|
3399
|
+
createInnerAgent(model, maxSteps) {
|
|
3263
3400
|
return new ToolLoopAgent({
|
|
3264
3401
|
model,
|
|
3265
3402
|
instructions: this.instructions,
|
|
3266
3403
|
tools: this.tools,
|
|
3267
|
-
stopWhen: stepCountIs(
|
|
3404
|
+
stopWhen: stepCountIs(maxSteps),
|
|
3268
3405
|
callOptionsSchema: ContractSpecCallOptionsSchema,
|
|
3269
3406
|
onStepFinish: async (step) => {
|
|
3270
3407
|
await this.handleStepFinish(step);
|
|
@@ -3272,6 +3409,10 @@ ${params.prompt}` : params.prompt;
|
|
|
3272
3409
|
});
|
|
3273
3410
|
}
|
|
3274
3411
|
async resolveModelForCall(params) {
|
|
3412
|
+
if (this.config.modelSelector && params.options?.selectionContext) {
|
|
3413
|
+
const { model } = await this.config.modelSelector.selectAndCreate(params.options.selectionContext);
|
|
3414
|
+
return model;
|
|
3415
|
+
}
|
|
3275
3416
|
const posthogConfig = this.config.posthogConfig;
|
|
3276
3417
|
if (!posthogConfig) {
|
|
3277
3418
|
return this.config.model;
|
|
@@ -3296,6 +3437,46 @@ ${params.prompt}` : params.prompt;
|
|
|
3296
3437
|
return createPostHogTracedModel2(this.config.model, posthogConfig, tracingOptions);
|
|
3297
3438
|
}
|
|
3298
3439
|
}
|
|
3440
|
+
function resolveMaxSteps(overrideMaxSteps, specMaxSteps) {
|
|
3441
|
+
const candidate = overrideMaxSteps ?? specMaxSteps ?? 10;
|
|
3442
|
+
if (!Number.isFinite(candidate)) {
|
|
3443
|
+
return 10;
|
|
3444
|
+
}
|
|
3445
|
+
if (candidate < 1) {
|
|
3446
|
+
return 1;
|
|
3447
|
+
}
|
|
3448
|
+
return Math.round(candidate);
|
|
3449
|
+
}
|
|
3450
|
+
function resolveEscalationError(spec, finishReason) {
|
|
3451
|
+
const escalation = spec.policy?.escalation;
|
|
3452
|
+
if (!escalation) {
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
if (escalation.onTimeout && finishReason === "length") {
|
|
3456
|
+
return {
|
|
3457
|
+
kind: "timeout",
|
|
3458
|
+
code: "AGENT_TIMEOUT_ESCALATION",
|
|
3459
|
+
message: "Agent reached max step budget and requires escalation."
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
if (escalation.onToolFailure && finishReason === "error") {
|
|
3463
|
+
return {
|
|
3464
|
+
kind: "retryable",
|
|
3465
|
+
code: "AGENT_TOOL_FAILURE_ESCALATION",
|
|
3466
|
+
message: "Agent encountered a tool failure and requires escalation."
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
const confidenceThreshold = escalation.confidenceThreshold;
|
|
3470
|
+
const defaultConfidence = spec.policy?.confidence?.default;
|
|
3471
|
+
if (confidenceThreshold !== undefined && defaultConfidence !== undefined && defaultConfidence < confidenceThreshold) {
|
|
3472
|
+
return {
|
|
3473
|
+
kind: "policy_blocked",
|
|
3474
|
+
code: "AGENT_CONFIDENCE_ESCALATION",
|
|
3475
|
+
message: `Agent default confidence (${defaultConfidence}) is below escalation threshold (${confidenceThreshold}).`
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
return;
|
|
3479
|
+
}
|
|
3299
3480
|
var ContractSpecCallOptionsSchema;
|
|
3300
3481
|
var init_contract_spec_agent = __esm(() => {
|
|
3301
3482
|
init_spec();
|
|
@@ -4724,7 +4905,10 @@ class UnifiedAgent {
|
|
|
4724
4905
|
async resolveAISDKModel() {
|
|
4725
4906
|
const backendConfig = this.getAISDKConfig();
|
|
4726
4907
|
let model;
|
|
4727
|
-
if (backendConfig?.
|
|
4908
|
+
if (backendConfig?.modelSelector && backendConfig.selectionContext) {
|
|
4909
|
+
const result = await backendConfig.modelSelector.selectAndCreate(backendConfig.selectionContext);
|
|
4910
|
+
model = result.model;
|
|
4911
|
+
} else if (backendConfig?.modelInstance) {
|
|
4728
4912
|
model = backendConfig.modelInstance;
|
|
4729
4913
|
} else if (backendConfig?.provider) {
|
|
4730
4914
|
const { createProvider } = await import("@contractspec/lib.ai-providers/factory");
|