@theoremjs/agents 0.3.4-next.484.g20d8b9d → 0.3.4-next.522.g9e684cd
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 +61 -51
- package/esm/mod.d.ts +2 -2
- package/esm/mod.js +1 -1
- package/esm/src/guardrails/credential-scan.d.ts +1 -1
- package/esm/src/guardrails/credential-scan.js +41 -18
- package/esm/src/guardrails/destinations.js +6 -2
- package/esm/src/guardrails/detect-at.d.ts +5 -5
- package/esm/src/guardrails/detect-at.js +51 -41
- package/esm/src/guardrails/detect-reply.d.ts +1 -1
- package/esm/src/guardrails/detect-reply.js +4 -2
- package/esm/src/guardrails/egress-automata.js +203 -202
- package/esm/src/guardrails/egress-compiler.js +1 -4
- package/esm/src/guardrails/egress-stream.js +10 -3
- package/esm/src/guardrails/egress-urls.js +4 -2
- package/esm/src/guardrails/error.d.ts +1 -1
- package/esm/src/guardrails/error.js +19 -2
- package/esm/src/guardrails/host-patterns.d.ts +1 -4
- package/esm/src/guardrails/host-patterns.js +1 -10
- package/esm/src/guardrails/injection-patterns.d.ts +3 -1
- package/esm/src/guardrails/injection-patterns.js +87 -4
- package/esm/src/guardrails/labels.d.ts +2 -2
- package/esm/src/guardrails/labels.js +7 -5
- package/esm/src/guardrails/mod.d.ts +2 -2
- package/esm/src/guardrails/mod.js +1 -1
- package/esm/src/guardrails/normalize.js +4 -2
- package/esm/src/guardrails/prompt-echo.js +4 -2
- package/esm/src/guardrails/sanitize.d.ts +1 -1
- package/esm/src/guardrails/sanitize.js +4 -3
- package/esm/src/guardrails/sensitive.d.ts +2 -2
- package/esm/src/guardrails/sensitive.js +1 -1
- package/esm/src/host/client-turn.js +8 -1
- package/esm/src/interface/profile-interface.js +13 -0
- package/esm/src/kernel/answer-tool.d.ts +14 -0
- package/esm/src/kernel/answer-tool.js +26 -0
- package/esm/src/kernel/engine/decision-references.d.ts +14 -0
- package/esm/src/kernel/engine/decision-references.js +55 -0
- package/esm/src/kernel/engine/decision.d.ts +3 -3
- package/esm/src/kernel/engine/decision.js +38 -22
- package/esm/src/kernel/engine/embedding.js +0 -2
- package/esm/src/kernel/engine/runner/gates.js +40 -5
- package/esm/src/kernel/engine/runner/mod.js +13 -5
- package/esm/src/kernel/engine/runner/state.d.ts +9 -1
- package/esm/src/kernel/engine/runner/state.js +10 -1
- package/esm/src/kernel/engine/runner/steps.js +50 -13
- package/esm/src/kernel/engine/runner/stream.d.ts +14 -8
- package/esm/src/kernel/engine/runner/stream.js +25 -13
- package/esm/src/kernel/engine/turn-trace.js +2 -2
- package/esm/src/kernel/mod.d.ts +1 -1
- package/esm/src/kernel/mod.js +1 -1
- package/esm/src/kernel/profile-presence.js +12 -0
- package/esm/src/kernel/profile-scope.js +14 -14
- package/esm/src/kernel/protocols/binding.d.ts +3 -1
- package/esm/src/kernel/protocols/binding.js +6 -0
- package/esm/src/kernel/protocols/limits.d.ts +10 -11
- package/esm/src/kernel/protocols/limits.js +9 -23
- package/esm/src/kernel/protocols/runtime.d.ts +3 -2
- package/esm/src/kernel/protocols/runtime.js +3 -4
- package/esm/src/kernel/provider-budget.d.ts +2 -2
- package/esm/src/kernel/provider-budget.js +22 -12
- package/esm/src/kernel/provider-runtime.js +8 -5
- package/esm/src/kernel/registry/profiles.js +3 -3
- package/esm/src/kernel/registry/resolve.js +4 -0
- package/esm/src/kernel/registry/schemas.js +1 -1
- package/esm/src/kernel/schema.js +28 -2
- package/esm/src/kernel/tools/execute.js +3 -1
- package/esm/src/kernel/tools/image-fetch.d.ts +2 -4
- package/esm/src/kernel/tools/image-fetch.js +19 -20
- package/esm/src/kernel/tools/mcp-sessions.d.ts +7 -2
- package/esm/src/kernel/tools/mcp-sessions.js +21 -12
- package/esm/src/kernel/tools/registry.js +21 -0
- package/esm/src/kernel/tools/remote.d.ts +0 -2
- package/esm/src/kernel/tools/remote.js +28 -23
- package/esm/src/kernel/tools/types.d.ts +30 -4
- package/esm/src/kernel/turn-events.d.ts +2 -0
- package/esm/src/kernel/turn-events.js +1 -0
- package/esm/src/kernel/types.d.ts +4 -2
- package/esm/src/observability/spans.js +7 -8
- package/esm/src/observability/trace-catalog.js +1 -0
- package/esm/src/providers/embedding-limits.js +4 -4
- package/esm/src/providers/gemini/{embedding-protocol.d.ts → embeddings-protocol.d.ts} +3 -3
- package/esm/src/providers/gemini/{embedding-protocol.js → embeddings-protocol.js} +3 -3
- package/esm/src/providers/gemini/live-protocol.js +4 -1
- package/esm/src/providers/gemini/provider.js +2 -2
- package/esm/src/providers/local/embeddings-protocol.d.ts +7 -0
- package/esm/src/providers/local/{embedding-protocol.js → embeddings-protocol.js} +2 -2
- package/esm/src/providers/local/provider.js +2 -2
- package/esm/src/providers/mod.d.ts +3 -3
- package/esm/src/providers/mod.js +3 -3
- package/esm/src/providers/openrouter/decisions-protocol.js +11 -8
- package/esm/src/providers/openrouter/embeddings-protocol.d.ts +6 -0
- package/esm/src/providers/openrouter/{embedding-protocol.js → embeddings-protocol.js} +2 -2
- package/esm/src/providers/openrouter/provider.js +2 -2
- package/package.json +1 -1
- package/esm/src/providers/local/embedding-protocol.d.ts +0 -7
- package/esm/src/providers/openrouter/embedding-protocol.d.ts +0 -6
|
@@ -5,7 +5,8 @@ import { guardedCall } from '../provider-runtime.js';
|
|
|
5
5
|
import { soleModelId } from '../registry/sole-model.js';
|
|
6
6
|
import { isRecord } from '../util/record.js';
|
|
7
7
|
import { tracedCall } from './call-trace.js';
|
|
8
|
-
|
|
8
|
+
import { referencedAssets } from './decision-references.js';
|
|
9
|
+
/** Each decision failure code, and the error kind it reads as. */
|
|
9
10
|
const DECISION_ERROR_KINDS = {
|
|
10
11
|
invalid_request: 'request',
|
|
11
12
|
authentication: 'auth',
|
|
@@ -65,7 +66,8 @@ function distribution(value, labels, path) {
|
|
|
65
66
|
for (const label of labels)
|
|
66
67
|
out[label] = finite(value[label], `${path}.${label}`);
|
|
67
68
|
const total = Object.values(out).reduce((sum, item) => sum + item, 0);
|
|
68
|
-
|
|
69
|
+
// why: Answers come rounded to hundredths, so each label may be off by half of one.
|
|
70
|
+
if (Math.abs(total - 1) > 0.005 * labels.length + 1e-9) {
|
|
69
71
|
throw new DecisionError('malformed_response', `${path} must sum to one`); // labels-exempt: upstream contract error
|
|
70
72
|
}
|
|
71
73
|
return out;
|
|
@@ -310,19 +312,19 @@ function isDecisionEntry(value) {
|
|
|
310
312
|
}
|
|
311
313
|
return true;
|
|
312
314
|
}
|
|
313
|
-
async function enforceDisclosure(profile, model, provider, request) {
|
|
315
|
+
async function enforceDisclosure(profile, model, provider, request, assets) {
|
|
314
316
|
const verdict = await profile.guardrails?.disclosure?.enforce(request.state, {
|
|
315
317
|
destination: provider,
|
|
316
318
|
profileId: profile.id,
|
|
317
319
|
model,
|
|
318
320
|
questionIds: Object.keys(request.questions),
|
|
319
|
-
...(
|
|
321
|
+
...(assets ? { assets } : {}),
|
|
320
322
|
});
|
|
321
323
|
if (verdict?.action === 'block') {
|
|
322
324
|
throw new DecisionError('disclosure_blocked', 'Decision disclosure was blocked'); // labels-exempt: developer contract error
|
|
323
325
|
}
|
|
324
326
|
}
|
|
325
|
-
function decisionSpanAttributes(profile, modelId, binding, request) {
|
|
327
|
+
function decisionSpanAttributes(profile, modelId, binding, request, assets) {
|
|
326
328
|
return {
|
|
327
329
|
'gen_ai.operation.name': 'decide',
|
|
328
330
|
'gen_ai.provider.name': binding.provider,
|
|
@@ -332,10 +334,10 @@ function decisionSpanAttributes(profile, modelId, binding, request) {
|
|
|
332
334
|
'theorem.decision.contract': profile.decision.contract,
|
|
333
335
|
'theorem.decision.state': traceJson(request.state),
|
|
334
336
|
'theorem.decision.questions': traceJson(request.questions),
|
|
335
|
-
...(
|
|
337
|
+
...(assets
|
|
336
338
|
? {
|
|
337
339
|
// invariant: Image bytes never reach a trace; it keeps each image's type and size.
|
|
338
|
-
'theorem.decision.assets': traceJson(Object.fromEntries(Object.entries(
|
|
340
|
+
'theorem.decision.assets': traceJson(Object.fromEntries(Object.entries(assets).map(([reference, { mimeType, data }]) => [
|
|
339
341
|
reference,
|
|
340
342
|
{ mimeType, bytes: data.length },
|
|
341
343
|
]))),
|
|
@@ -384,25 +386,38 @@ function decisionProviderError(error) {
|
|
|
384
386
|
// why: A framework failure's message holds no response body, and says what stopped the decision.
|
|
385
387
|
error instanceof TheoremError ? error.message : labelText('provider.decision_failed'), isRecord(error) && typeof error.status === 'number' ? error.status : undefined, { cause: error });
|
|
386
388
|
}
|
|
387
|
-
|
|
389
|
+
/** What the provider gets: each box's image said in its instructions, and only the images a string references. */
|
|
390
|
+
function sentCall(request) {
|
|
391
|
+
const questions = Object.fromEntries(Object.entries(request.questions).map(([id, question]) => [
|
|
392
|
+
id,
|
|
393
|
+
providerQuestion(question, request.assets),
|
|
394
|
+
]));
|
|
395
|
+
const assets = request.assets
|
|
396
|
+
? referencedAssets([
|
|
397
|
+
request.state,
|
|
398
|
+
...Object.values(questions).flatMap((question) => [
|
|
399
|
+
question.instructions,
|
|
400
|
+
'criteria' in question ? question.criteria : undefined,
|
|
401
|
+
]),
|
|
402
|
+
], request.assets)
|
|
403
|
+
: {};
|
|
404
|
+
return {
|
|
405
|
+
state: request.state,
|
|
406
|
+
questions,
|
|
407
|
+
...(Object.keys(assets).length ? { assets } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async function decide(registry, profile, [modelId, binding], request, sent, options) {
|
|
388
411
|
throwIfAborted(request.signal);
|
|
389
412
|
const provider = registry.providers.require(binding.provider, binding.apiId);
|
|
390
|
-
await enforceDisclosure(profile, modelId, binding.provider, request);
|
|
413
|
+
await enforceDisclosure(profile, modelId, binding.provider, request, sent.assets);
|
|
391
414
|
throwIfAborted(request.signal);
|
|
392
415
|
try {
|
|
393
416
|
const call = guardedCall(provider, binding, 'decision', options, {
|
|
394
417
|
...(request.signal ? { signal: request.signal } : {}),
|
|
395
418
|
...(binding.timeoutMs ? { timeoutMs: binding.timeoutMs } : {}),
|
|
396
419
|
});
|
|
397
|
-
const result = await call.decide({
|
|
398
|
-
apiId: binding.apiId,
|
|
399
|
-
state: request.state,
|
|
400
|
-
questions: Object.fromEntries(Object.entries(request.questions).map(([id, question]) => [
|
|
401
|
-
id,
|
|
402
|
-
providerQuestion(question, request.assets),
|
|
403
|
-
])),
|
|
404
|
-
...(request.assets ? { assets: request.assets } : {}),
|
|
405
|
-
});
|
|
420
|
+
const result = await call.decide({ apiId: binding.apiId, ...sent });
|
|
406
421
|
if (typeof result.model !== 'string' || !result.model.trim())
|
|
407
422
|
throw new DecisionError('malformed_response', labelText('provider.decision_model_missing'));
|
|
408
423
|
return { ...result, answers: validateAnswers(result.answers, request.questions) };
|
|
@@ -414,17 +429,18 @@ async function decide(registry, profile, [modelId, binding], request, options) {
|
|
|
414
429
|
}
|
|
415
430
|
}
|
|
416
431
|
/**
|
|
417
|
-
*
|
|
418
|
-
*
|
|
432
|
+
* One decision, traced however it ends. A transient failure (408, 429, a 5xx gateway or a network
|
|
433
|
+
* error) is retried within five seconds; a trace write failure never changes the decision's outcome.
|
|
419
434
|
*/
|
|
420
435
|
export async function runDecisionInRegistry(registry, request, options) {
|
|
421
436
|
const profile = requireDecisionProfile(registry, request.profile);
|
|
422
437
|
validateDecisionRequest(request, profile);
|
|
423
438
|
const model = decisionModel(profile);
|
|
424
439
|
const [modelId, binding] = model;
|
|
440
|
+
const sent = sentCall(request);
|
|
425
441
|
return await tracedCall({
|
|
426
442
|
name: `decide ${binding.apiId}`,
|
|
427
|
-
attributes: decisionSpanAttributes(profile, modelId, binding, request),
|
|
443
|
+
attributes: decisionSpanAttributes(profile, modelId, binding, request, sent.assets),
|
|
428
444
|
traceparent: request.traceparent,
|
|
429
445
|
metadata: request.metadata,
|
|
430
446
|
writer: {
|
|
@@ -433,7 +449,7 @@ export async function runDecisionInRegistry(registry, request, options) {
|
|
|
433
449
|
guardrails: profile.guardrails,
|
|
434
450
|
},
|
|
435
451
|
}, async (root) => {
|
|
436
|
-
const result = await decide(registry, profile, model, request, options);
|
|
452
|
+
const result = await decide(registry, profile, model, request, sent, options);
|
|
437
453
|
recordResult(root, result);
|
|
438
454
|
return result;
|
|
439
455
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { refuseOverlong } from '../../guardrails/detect-at.js';
|
|
2
1
|
import { TheoremError, throwIfAborted } from '../../guardrails/error.js';
|
|
3
2
|
import { protocolOf } from '../protocols/binding.js';
|
|
4
3
|
import { guardedCall } from '../provider-runtime.js';
|
|
@@ -47,7 +46,6 @@ function validateInputs(profile, modelId, inputs, model) {
|
|
|
47
46
|
const files = [];
|
|
48
47
|
for (const part of parts) {
|
|
49
48
|
if (part.type === 'text') {
|
|
50
|
-
refuseOverlong(part.text, 'embedding input');
|
|
51
49
|
if (!rules.text)
|
|
52
50
|
throw new TheoremError('unsupported', `Profile ${profile.id} does not accept text embedding input`);
|
|
53
51
|
continue;
|
|
@@ -8,6 +8,7 @@ import { labelText } from '../../../guardrails/labels.js';
|
|
|
8
8
|
import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
|
|
9
9
|
import { replyIsJudged } from '../../../guardrails/progressive-yield.js';
|
|
10
10
|
import { sanitizeTurnRequest } from '../../../guardrails/sanitize.js';
|
|
11
|
+
import { ANSWER_TOOL, answersByTool } from '../../answer-tool.js';
|
|
11
12
|
import { resolveInputParts } from '../../registry/ingress.js';
|
|
12
13
|
import { profileTurnOutputs } from '../../registry/profile-outputs.js';
|
|
13
14
|
import { injectWouldExceedMaxSteps } from '../../stages.js';
|
|
@@ -15,7 +16,7 @@ import { findLast } from '../../util/find-last.js';
|
|
|
15
16
|
import { guardrailCheckAttributes } from '../turn-trace.js';
|
|
16
17
|
import { collectItemFailures, collectValidationFailures, formatValidationFailures, itemSchemaAt, valueAt, withListAt, } from './schema-validation.js';
|
|
17
18
|
import { applyTurnStage } from './stages.js';
|
|
18
|
-
import { appendUserInput, } from './state.js';
|
|
19
|
+
import { appendUserInput, recordStepEvent, } from './state.js';
|
|
19
20
|
import { executeAttempt } from './steps.js';
|
|
20
21
|
import { isWithheldOnBlock, shownToHost } from './stream.js';
|
|
21
22
|
/** A turn whose final output the egress check blocked (withheld or replaced by policy copy). */
|
|
@@ -152,7 +153,8 @@ function updateFlowForRetry(flow, state, profile, nextReq, reason) {
|
|
|
152
153
|
flow.currentReq = nextReq;
|
|
153
154
|
const safe = sanitizeTurnRequest(nextReq, profile);
|
|
154
155
|
if (profile.type === 'text') {
|
|
155
|
-
// why: The
|
|
156
|
+
// why: The repair is the turn's next user message, and the retry reads this turn alone.
|
|
157
|
+
state.turnOnly = true;
|
|
156
158
|
const before = state.currentHistory.length;
|
|
157
159
|
appendUserInput(state, resolveInputParts(profile, { ...safe, input: { repair: safe.input?.repair } }));
|
|
158
160
|
for (const message of state.currentHistory.slice(before))
|
|
@@ -386,6 +388,31 @@ async function handleValidationGate(validation, flow, state, profile, latestStru
|
|
|
386
388
|
holdReply(state, withoutItems(reply, refused), refused, outcome.failed);
|
|
387
389
|
return 'pass';
|
|
388
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* An attempt on a turn that answers by tool, ended without calling the answer tool: a repair while
|
|
393
|
+
* `outputs.validation` has a retry left, else a `bad_response` failure.
|
|
394
|
+
*/
|
|
395
|
+
function* handleMissingAnswer(flow, state, profile, latestStructured,
|
|
396
|
+
/** True when the attempt's events go out at its end (`yieldBufferedAttemptEvents`). */
|
|
397
|
+
held) {
|
|
398
|
+
if (latestStructured !== undefined || state.lastStop?.kind !== 'completed')
|
|
399
|
+
return 'pass';
|
|
400
|
+
if (!answersByTool(flow.currentGen))
|
|
401
|
+
return 'pass';
|
|
402
|
+
const validation = profileTurnOutputs(profile)?.validation;
|
|
403
|
+
if (validation && flow.currentAttempt < (validation.maxRetries ?? 0)) {
|
|
404
|
+
const rejection = labelText('structured.answer_missing', { tool: ANSWER_TOOL }, profile.labels);
|
|
405
|
+
const nextRequest = buildRepairRequest(flow.currentReq, collectAttemptText(state.attemptEvents), rejection);
|
|
406
|
+
yield updateFlowForRetry(flow, state, profile, nextRequest, 'validation');
|
|
407
|
+
return 'continue';
|
|
408
|
+
}
|
|
409
|
+
const failure = toErrorEvent(new TheoremError('bad_response', `the model ended without calling ${ANSWER_TOOL}`));
|
|
410
|
+
recordStepEvent(failure, state);
|
|
411
|
+
state.lastStop = { kind: 'provider_error' };
|
|
412
|
+
if (!held)
|
|
413
|
+
yield failure;
|
|
414
|
+
return 'pass';
|
|
415
|
+
}
|
|
389
416
|
/** Puts on the attempt's structured output the fields that still failed. */
|
|
390
417
|
function markFailed(state, failed) {
|
|
391
418
|
const held = findLast(state.attemptEvents, (event) => event.type === 'structured');
|
|
@@ -403,9 +430,6 @@ function gateStatusToAction(status) {
|
|
|
403
430
|
}
|
|
404
431
|
async function* executeSingleAttemptCycle(args) {
|
|
405
432
|
const { flow, state, profile, system, provider } = args;
|
|
406
|
-
const validation = profileTurnOutputs(profile)?.validation;
|
|
407
|
-
const policy = resolveGuardrailPolicy(profile.guardrails);
|
|
408
|
-
const judged = replyIsJudged(policy, TURN_REPLY);
|
|
409
433
|
// invariant: Fresh maxSteps budget per validation/egress attempt. before_end inject
|
|
410
434
|
// re-entry inside this cycle still accumulates stepCount (do not reset there).
|
|
411
435
|
state.stepCount = 0;
|
|
@@ -465,6 +489,14 @@ async function* executeSingleAttemptCycle(args) {
|
|
|
465
489
|
}
|
|
466
490
|
break;
|
|
467
491
|
}
|
|
492
|
+
return yield* checkAttemptReply(flow, state, profile, system, latestStructured);
|
|
493
|
+
}
|
|
494
|
+
/** The checks on an attempt's finished reply: the egress judge, the answer, then validation. */
|
|
495
|
+
async function* checkAttemptReply(flow, state, profile, system, structured) {
|
|
496
|
+
const validation = profileTurnOutputs(profile)?.validation;
|
|
497
|
+
const policy = resolveGuardrailPolicy(profile.guardrails);
|
|
498
|
+
const judged = replyIsJudged(policy, TURN_REPLY);
|
|
499
|
+
let latestStructured = structured;
|
|
468
500
|
if (judged) {
|
|
469
501
|
const status = yield* handleEgressGate(policy, flow, state, profile, system.private);
|
|
470
502
|
const action = gateStatusToAction(status);
|
|
@@ -473,6 +505,9 @@ async function* executeSingleAttemptCycle(args) {
|
|
|
473
505
|
}
|
|
474
506
|
latestStructured = projectOutbound(state.attemptEvents).structured ?? latestStructured;
|
|
475
507
|
}
|
|
508
|
+
if ((yield* handleMissingAnswer(flow, state, profile, latestStructured, Boolean(validation) || judged)) === 'continue') {
|
|
509
|
+
return { status: 'continue' };
|
|
510
|
+
}
|
|
476
511
|
if (validation) {
|
|
477
512
|
const status = await handleValidationGate(validation, flow, state, profile, latestStructured);
|
|
478
513
|
if (status !== 'pass') {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canaryNote, contextNote, profileCanary, userDataNote, } from '../../../guardrails/canary.js';
|
|
2
|
-
import { isAbortError, TheoremError, throwIfAborted, withPublicLabels, } from '../../../guardrails/error.js';
|
|
2
|
+
import { describeError, errorKind, isAbortError, TheoremError, throwIfAborted, withPublicLabels, } from '../../../guardrails/error.js';
|
|
3
3
|
import { projectGuardrailTurnEvent } from '../../../guardrails/events.js';
|
|
4
4
|
import { labelText } from '../../../guardrails/labels.js';
|
|
5
5
|
import { sanitizeTurnRequestWithEvents } from '../../../guardrails/sanitize.js';
|
|
@@ -22,7 +22,7 @@ import { sumEventTokens, sumTurnTokens } from '../usage.js';
|
|
|
22
22
|
import { runAttemptsWithValidation } from './gates.js';
|
|
23
23
|
import { createAgentCaller, NESTED_THROWS, turnError } from './nested.js';
|
|
24
24
|
import { applyTurnStage } from './stages.js';
|
|
25
|
-
import { openTurnState } from './state.js';
|
|
25
|
+
import { cutConversation, openTurnState, } from './state.js';
|
|
26
26
|
import { shouldSkipStreamEvent } from './stream.js';
|
|
27
27
|
function projectForObs(event, policy) {
|
|
28
28
|
return projectGuardrailTurnEvent(event, policy?.include.guardrailMatchPreview ?? false);
|
|
@@ -105,10 +105,10 @@ async function runCompactor(args) {
|
|
|
105
105
|
}
|
|
106
106
|
catch (err) {
|
|
107
107
|
throwIfAborted(args.signal);
|
|
108
|
-
const error = err
|
|
108
|
+
const error = errorKind(err);
|
|
109
109
|
if (NESTED_THROWS.has(error))
|
|
110
110
|
throw err;
|
|
111
|
-
return { droppedMedia, failure: { error } };
|
|
111
|
+
return { droppedMedia, failure: { error, errorInternal: describeError(err) } };
|
|
112
112
|
}
|
|
113
113
|
throwIfAborted(args.signal);
|
|
114
114
|
const tokens = sumEventTokens(events);
|
|
@@ -122,7 +122,11 @@ async function runCompactor(args) {
|
|
|
122
122
|
if (stop !== 'completed' || error) {
|
|
123
123
|
return {
|
|
124
124
|
droppedMedia,
|
|
125
|
-
failure: {
|
|
125
|
+
failure: {
|
|
126
|
+
...(stop ? { stop } : {}),
|
|
127
|
+
...(error ? { error } : {}),
|
|
128
|
+
...(reported?.errorInternal ? { errorInternal: reported.errorInternal } : {}),
|
|
129
|
+
},
|
|
126
130
|
...usage,
|
|
127
131
|
};
|
|
128
132
|
}
|
|
@@ -163,6 +167,9 @@ function compactionOutcome(result, messagesBefore) {
|
|
|
163
167
|
dropped_media: result.droppedMedia,
|
|
164
168
|
...(result.failure?.stop ? { failure_stop: result.failure.stop } : {}),
|
|
165
169
|
...(result.failure?.error ? { failure_error: result.failure.error } : {}),
|
|
170
|
+
...(result.failure?.errorInternal
|
|
171
|
+
? { failure_reason: traceContent(result.failure.errorInternal) }
|
|
172
|
+
: {}),
|
|
166
173
|
...(result.failure?.empty ? { failure_empty: true } : {}),
|
|
167
174
|
...(result.failure?.unreadable ? { failure_unreadable: true } : {}),
|
|
168
175
|
...(result.summary === undefined ? {} : { summary: traceContent(result.summary) }),
|
|
@@ -274,6 +281,7 @@ async function* emitTurn(args) {
|
|
|
274
281
|
const done = turnDoneOf({
|
|
275
282
|
stop: state.lastStop ?? { kind: 'completed' },
|
|
276
283
|
...(state.providerState &&
|
|
284
|
+
!cutConversation(state) &&
|
|
277
285
|
!('providerContinuation' in profile && profile.providerContinuation?.history === 'stateless') &&
|
|
278
286
|
(!state.lastStop || ['completed', 'tool', 'gate'].includes(state.lastStop.kind))
|
|
279
287
|
? { providerState: state.providerState }
|
|
@@ -23,6 +23,10 @@ interface StepExecutionState {
|
|
|
23
23
|
agents?: AgentCaller;
|
|
24
24
|
trace: TurnTraceState;
|
|
25
25
|
currentHistory: TurnHistoryMessage[];
|
|
26
|
+
/** Where this turn begins in `currentHistory`: what came before is the conversation the host passed. */
|
|
27
|
+
turnStart: number;
|
|
28
|
+
/** Set by a retry: from then on the model reads this turn only, from `turnStart`. */
|
|
29
|
+
turnOnly?: boolean;
|
|
26
30
|
stepCount: number;
|
|
27
31
|
/** Model calls sent again this turn under the profile's resumption (`executeResumedStep`). */
|
|
28
32
|
resumes: number;
|
|
@@ -106,5 +110,9 @@ declare function openTurnState(args: {
|
|
|
106
110
|
allEmittedEvents?: TurnEvent[];
|
|
107
111
|
agents?: AgentCaller;
|
|
108
112
|
}): StepExecutionState;
|
|
113
|
+
/** The history the next model call reads: this turn alone once a retry has cut it. */
|
|
114
|
+
declare function modelHistory(state: StepExecutionState): TurnHistoryMessage[];
|
|
115
|
+
/** Whether the turn's checkpoint covers less than the conversation the host keeps. */
|
|
116
|
+
declare function cutConversation(state: StepExecutionState): boolean;
|
|
109
117
|
export type { AttemptFlowState, PendingItemFix, StepExecutionState, TurnTraceState };
|
|
110
|
-
export { appendUserInput, openTurnState, recordStepEvent };
|
|
118
|
+
export { appendUserInput, cutConversation, modelHistory, openTurnState, recordStepEvent };
|
|
@@ -28,6 +28,7 @@ function openTurnState(args) {
|
|
|
28
28
|
tools: args.tools,
|
|
29
29
|
trace: args.trace,
|
|
30
30
|
currentHistory: [...(generation.history ?? [])],
|
|
31
|
+
turnStart: generation.history?.length ?? 0,
|
|
31
32
|
stepCount: 0,
|
|
32
33
|
resumes: 0,
|
|
33
34
|
toolResults: [],
|
|
@@ -49,4 +50,12 @@ function openTurnState(args) {
|
|
|
49
50
|
}
|
|
50
51
|
return state;
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
/** The history the next model call reads: this turn alone once a retry has cut it. */
|
|
54
|
+
function modelHistory(state) {
|
|
55
|
+
return state.turnOnly ? state.currentHistory.slice(state.turnStart) : state.currentHistory;
|
|
56
|
+
}
|
|
57
|
+
/** Whether the turn's checkpoint covers less than the conversation the host keeps. */
|
|
58
|
+
function cutConversation(state) {
|
|
59
|
+
return state.turnOnly === true && state.turnStart > 0;
|
|
60
|
+
}
|
|
61
|
+
export { appendUserInput, cutConversation, modelHistory, openTurnState, recordStepEvent };
|
|
@@ -7,6 +7,7 @@ import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
|
|
|
7
7
|
import { replyIsJudged } from '../../../guardrails/progressive-yield.js';
|
|
8
8
|
import { recordTaint } from '../../../guardrails/tool-result.js';
|
|
9
9
|
import { waitDefault } from '../../../providers/shared/retry.js';
|
|
10
|
+
import { ANSWER_TOOL, answersByTool, withAnswerTool } from '../../answer-tool.js';
|
|
10
11
|
import { stepReplyMessages } from '../../interaction-parts.js';
|
|
11
12
|
import { profileTurnOutputs } from '../../registry/profile-outputs.js';
|
|
12
13
|
import { providerCompleteRequest } from '../../registry/provider-request.js';
|
|
@@ -20,7 +21,7 @@ import { findLast } from '../../util/find-last.js';
|
|
|
20
21
|
import { startToolTrace } from '../tool-trace.js';
|
|
21
22
|
import { startCallTrace } from '../turn-trace.js';
|
|
22
23
|
import { applyStageInjects, recordReply } from './stages.js';
|
|
23
|
-
import { recordStepEvent } from './state.js';
|
|
24
|
+
import { modelHistory, recordStepEvent } from './state.js';
|
|
24
25
|
import { inStep, runSideBySide, stepOrder } from './step-order.js';
|
|
25
26
|
import { isWithheldOnBlock, StructuredDrafts, yieldProviderEvents, } from './stream.js';
|
|
26
27
|
import { callTokensEvent, observeCallEvent, startCallUsage } from './usage.js';
|
|
@@ -29,7 +30,7 @@ function isStepLimitReached(step, maxSteps) {
|
|
|
29
30
|
}
|
|
30
31
|
/** The step's generation: the turn's history so far, and no tools when the step must answer. */
|
|
31
32
|
function generationForProviderStep(generation, state, answerOnly) {
|
|
32
|
-
const forStep = { ...generation, history: state
|
|
33
|
+
const forStep = { ...generation, history: modelHistory(state) };
|
|
33
34
|
if (!answerOnly)
|
|
34
35
|
return forStep;
|
|
35
36
|
return { ...forStep, builtins: [], tools: { ...generation.tools, wire: [] } };
|
|
@@ -60,14 +61,14 @@ function holdModelCall(calls, tool) {
|
|
|
60
61
|
return true;
|
|
61
62
|
}
|
|
62
63
|
/** The step's request, with what it gives the model noted on `state`, and its call's usage and trace. */
|
|
63
|
-
function startProviderCall(generation, system, state, answerOnly) {
|
|
64
|
+
function startProviderCall(generation, system, state, answerOnly, labels) {
|
|
64
65
|
const usage = startCallUsage(system.text, {
|
|
65
|
-
history: state
|
|
66
|
+
history: modelHistory(state),
|
|
66
67
|
input: generation.input,
|
|
67
68
|
});
|
|
68
69
|
state.lastCall = usage;
|
|
69
70
|
const genForStep = generationForProviderStep(generation, state, answerOnly);
|
|
70
|
-
const request = providerCompleteRequest(state.tools, genForStep, system.text);
|
|
71
|
+
const request = withAnswerTool(providerCompleteRequest(state.tools, genForStep, system.text), generation, labels);
|
|
71
72
|
addRequestUrls(state.givenUrls, request);
|
|
72
73
|
addRequestDestinations(state.destinations, request, state.givenUrls.own);
|
|
73
74
|
if (generation.canary && !state.canaryGiven) {
|
|
@@ -118,6 +119,42 @@ function tallyCallEvent(event, tally, call) {
|
|
|
118
119
|
tally.replied = true;
|
|
119
120
|
return false;
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The structured output the call gave through the answer tool, shown as the step's, with the answer
|
|
124
|
+
* tool's calls taken out of the step's calls. An answer ends the turn, so the step's other calls do
|
|
125
|
+
* not run, and the call that gave it completed.
|
|
126
|
+
*/
|
|
127
|
+
async function* showAnswer(shown, tally, step) {
|
|
128
|
+
if (!answersByTool(step.generation))
|
|
129
|
+
return;
|
|
130
|
+
const answer = tally.calls.find((call) => call.name === ANSWER_TOOL && !call.failure);
|
|
131
|
+
tally.calls = answer ? [] : tally.calls.filter((call) => call.name !== ANSWER_TOOL);
|
|
132
|
+
if (!answer)
|
|
133
|
+
return;
|
|
134
|
+
if (tally.stop?.kind === 'tool') {
|
|
135
|
+
tally.stop = { kind: 'completed' };
|
|
136
|
+
step.state.lastStop = tally.stop;
|
|
137
|
+
}
|
|
138
|
+
const event = { type: 'structured', structured: answer.arguments };
|
|
139
|
+
if (tallyCallEvent(event, tally, step.call))
|
|
140
|
+
return;
|
|
141
|
+
if (releasesNow(event, step.control, step.state, step.buffer))
|
|
142
|
+
yield* shown.show(event);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A tool call's preview. The answer tool's streams as the structured reply's draft.
|
|
146
|
+
*
|
|
147
|
+
* why: A preview of the call that follows, which owns the arguments: not the turn's record. It streams
|
|
148
|
+
* as the reply's text does, through its own gate, and like that text is held only when delivery is buffered.
|
|
149
|
+
*/
|
|
150
|
+
async function* showToolDraft(event, shown, generation, buffer) {
|
|
151
|
+
if (buffer.holdVisible)
|
|
152
|
+
return;
|
|
153
|
+
if (event.name === ANSWER_TOOL && answersByTool(generation))
|
|
154
|
+
yield* shown.draft(event.text);
|
|
155
|
+
else
|
|
156
|
+
yield event;
|
|
157
|
+
}
|
|
121
158
|
/** What sets one model call's text apart from the reply text before it: a blank line. */
|
|
122
159
|
const CALL_BREAK = '\n\n';
|
|
123
160
|
/**
|
|
@@ -130,7 +167,8 @@ function withCallBreak(event, tally, state, generation) {
|
|
|
130
167
|
if (tally.text)
|
|
131
168
|
return event;
|
|
132
169
|
const before = state.replyText;
|
|
133
|
-
const
|
|
170
|
+
const prose = generation.structured === null || answersByTool(generation);
|
|
171
|
+
const breaks = before && prose && !/\s$/.test(before) && !/^\s/.test(event.text);
|
|
134
172
|
tally.replyAt = before.length + (breaks ? CALL_BREAK.length : 0);
|
|
135
173
|
return breaks ? { ...event, text: `${CALL_BREAK}${event.text}` } : event;
|
|
136
174
|
}
|
|
@@ -194,7 +232,7 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
194
232
|
holdVisible: false,
|
|
195
233
|
}) {
|
|
196
234
|
const { profile, generation, system, provider, signal } = args;
|
|
197
|
-
const { usage, genForStep, request, call } = startProviderCall(generation, system, state, isAnswerOnlyStep(profile, generation, state.stepCount));
|
|
235
|
+
const { usage, genForStep, request, call } = startProviderCall(generation, system, state, isAnswerOnlyStep(profile, generation, state.stepCount), profile.labels);
|
|
198
236
|
const tally = { calls: [], reasoning: [], text: '', failures: [], replied: false };
|
|
199
237
|
const control = {
|
|
200
238
|
withholdVisible: false,
|
|
@@ -209,7 +247,7 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
209
247
|
canaryGiven: state.canaryGiven,
|
|
210
248
|
...(state.ownTools ? { ownTools: state.ownTools } : {}),
|
|
211
249
|
};
|
|
212
|
-
const shown = new StructuredDrafts(seen);
|
|
250
|
+
const shown = new StructuredDrafts({ ...seen, generation });
|
|
213
251
|
try {
|
|
214
252
|
for await (const event of yieldProviderEvents({
|
|
215
253
|
...seen,
|
|
@@ -219,6 +257,7 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
219
257
|
signal,
|
|
220
258
|
control,
|
|
221
259
|
tools: state.tools,
|
|
260
|
+
answerTool: answersByTool(generation),
|
|
222
261
|
})) {
|
|
223
262
|
if (observeCallEvent(usage, event))
|
|
224
263
|
continue;
|
|
@@ -234,14 +273,11 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
234
273
|
continue;
|
|
235
274
|
}
|
|
236
275
|
if (event.type === 'tool_draft') {
|
|
237
|
-
|
|
238
|
-
// as the reply's text does, through its own gate, and like that text is held only when delivery is buffered.
|
|
239
|
-
if (!buffer.holdVisible)
|
|
240
|
-
yield event;
|
|
276
|
+
yield* showToolDraft(event, shown, generation, buffer);
|
|
241
277
|
continue;
|
|
242
278
|
}
|
|
243
279
|
// why: The model's history keeps what it wrote; only the reply the host gets is set apart.
|
|
244
|
-
const next = event.type === 'text' ? withCallBreak(event, tally, state,
|
|
280
|
+
const next = event.type === 'text' ? withCallBreak(event, tally, state, generation) : event;
|
|
245
281
|
if (tallyCallEvent(event, tally, call))
|
|
246
282
|
continue;
|
|
247
283
|
if (!releasesNow(next, control, state, buffer))
|
|
@@ -254,6 +290,7 @@ async function* executeAutonomousStep(args, state, buffer = {
|
|
|
254
290
|
throw err;
|
|
255
291
|
}
|
|
256
292
|
yield* shown.end();
|
|
293
|
+
yield* showAnswer(shown, tally, { generation, state, control, buffer, call });
|
|
257
294
|
keepStreamCarry(state, control);
|
|
258
295
|
const resume = resumeOf(args.mayResume, profile, tally);
|
|
259
296
|
if (!resume) {
|
|
@@ -24,18 +24,20 @@ interface OutboundStreamControl {
|
|
|
24
24
|
declare function shouldSkipStreamEvent(event: ModelEvent, profile: Profile): boolean;
|
|
25
25
|
/** Host-visible output a mid-stream block withholds. A thought is never withheld, only omitted from. */
|
|
26
26
|
declare function isWithheldOnBlock(event: ModelEvent): boolean;
|
|
27
|
+
export type ShownGeneration = Pick<ResolvedGeneration, 'structured' | 'stream' | 'tools'>;
|
|
27
28
|
/**
|
|
28
|
-
* An event as the host gets it, or nothing.
|
|
29
|
-
* that output's JSON: under `sse` it goes out as `structured_draft`, and under `buffered`
|
|
30
|
-
* since the `structured` event is the reply whole. The turn keeps it as text, which its
|
|
31
|
-
* repairs and history read.
|
|
29
|
+
* An event as the host gets it, or nothing. On a turn that answers through a response format, the
|
|
30
|
+
* text is that output's JSON: under `sse` it goes out as `structured_draft`, and under `buffered`
|
|
31
|
+
* not at all, since the `structured` event is the reply whole. The turn keeps it as text, which its
|
|
32
|
+
* checks, repairs and history read. On a turn that answers by tool, the text is progress.
|
|
32
33
|
*/
|
|
33
|
-
declare function shownToHost(event: TurnEvent, generation:
|
|
34
|
+
declare function shownToHost(event: TurnEvent, generation: ShownGeneration): TurnEvent | undefined;
|
|
34
35
|
/**
|
|
35
36
|
* A step's events as the host gets them (`shownToHost`), with a structured reply's drafts read
|
|
36
|
-
* first at `reply_structured`, where the whole is read. The reply's text
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* first at `reply_structured`, where the whole is read. The drafts are the reply's text, or the
|
|
38
|
+
* answer tool's arguments as the model writes them. Text has already passed the `reply` gate; a
|
|
39
|
+
* match at `reply_structured` stops the drafts, and the `structured` event still arrives as that
|
|
40
|
+
* check leaves it. The held tail is read and released when the step's stream ends.
|
|
39
41
|
*/
|
|
40
42
|
declare class StructuredDrafts {
|
|
41
43
|
private readonly drafts;
|
|
@@ -43,6 +45,8 @@ declare class StructuredDrafts {
|
|
|
43
45
|
/** Reads the stream's own checks off what the step knows of the turn. */
|
|
44
46
|
constructor(seen: Pick<StreamArgs, 'profile' | 'generation' | 'privateSystem' | 'givenUrls' | 'canaryGiven' | 'ownTools'>);
|
|
45
47
|
show(event: TurnEvent): AsyncGenerator<TurnEvent>;
|
|
48
|
+
/** Part of the structured output's JSON, as the model writes it. */
|
|
49
|
+
draft(json: string): AsyncGenerator<TurnEvent>;
|
|
46
50
|
/** The drafts' tail the gate still holds, once the step's stream has ended. */
|
|
47
51
|
end(): Generator<TurnEvent>;
|
|
48
52
|
}
|
|
@@ -65,6 +69,8 @@ interface StreamArgs {
|
|
|
65
69
|
ownTools?: OwnTools;
|
|
66
70
|
/** The turn's tools: a draft is read at the boundary its tool's calls are. */
|
|
67
71
|
tools: ToolRegistry;
|
|
72
|
+
/** True when the turn answers through the answer tool (`answersByTool`). */
|
|
73
|
+
answerTool?: boolean;
|
|
68
74
|
}
|
|
69
75
|
declare function yieldProviderEvents(args: StreamArgs): AsyncGenerator<StreamEvent>;
|
|
70
76
|
export type { OutboundStreamControl, StreamArgs };
|
|
@@ -7,6 +7,7 @@ import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
|
|
|
7
7
|
import { createDraftGate, createOutboundProgressiveGate, DraftReader, } from '../../../guardrails/progressive-yield.js';
|
|
8
8
|
import { EGRESS_RULES } from '../../../guardrails/rules.js';
|
|
9
9
|
import { thoughtGuardFor, } from '../../../guardrails/thought-guard.js';
|
|
10
|
+
import { ANSWER_TOOL, answersByTool } from '../../answer-tool.js';
|
|
10
11
|
import { profileTurnOutputs } from '../../registry/profile-outputs.js';
|
|
11
12
|
import { toolArgumentsBoundary } from '../../tools/execute.js';
|
|
12
13
|
function shouldSkipStreamEvent(event, profile) {
|
|
@@ -69,21 +70,22 @@ function isWithheldOnBlock(event) {
|
|
|
69
70
|
return event.type === 'text' || event.type === 'media';
|
|
70
71
|
}
|
|
71
72
|
/**
|
|
72
|
-
* An event as the host gets it, or nothing.
|
|
73
|
-
* that output's JSON: under `sse` it goes out as `structured_draft`, and under `buffered`
|
|
74
|
-
* since the `structured` event is the reply whole. The turn keeps it as text, which its
|
|
75
|
-
* repairs and history read.
|
|
73
|
+
* An event as the host gets it, or nothing. On a turn that answers through a response format, the
|
|
74
|
+
* text is that output's JSON: under `sse` it goes out as `structured_draft`, and under `buffered`
|
|
75
|
+
* not at all, since the `structured` event is the reply whole. The turn keeps it as text, which its
|
|
76
|
+
* checks, repairs and history read. On a turn that answers by tool, the text is progress.
|
|
76
77
|
*/
|
|
77
78
|
function shownToHost(event, generation) {
|
|
78
|
-
if (!generation.structured || event.type !== 'text')
|
|
79
|
+
if (!generation.structured || answersByTool(generation) || event.type !== 'text')
|
|
79
80
|
return event;
|
|
80
81
|
return generation.stream === false ? undefined : { type: 'structured_draft', text: event.text };
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
83
84
|
* A step's events as the host gets them (`shownToHost`), with a structured reply's drafts read
|
|
84
|
-
* first at `reply_structured`, where the whole is read. The reply's text
|
|
85
|
-
*
|
|
86
|
-
*
|
|
85
|
+
* first at `reply_structured`, where the whole is read. The drafts are the reply's text, or the
|
|
86
|
+
* answer tool's arguments as the model writes them. Text has already passed the `reply` gate; a
|
|
87
|
+
* match at `reply_structured` stops the drafts, and the `structured` event still arrives as that
|
|
88
|
+
* check leaves it. The held tail is read and released when the step's stream ends.
|
|
87
89
|
*/
|
|
88
90
|
class StructuredDrafts {
|
|
89
91
|
drafts;
|
|
@@ -102,9 +104,15 @@ class StructuredDrafts {
|
|
|
102
104
|
yield shown;
|
|
103
105
|
return;
|
|
104
106
|
}
|
|
105
|
-
|
|
107
|
+
yield* this.draft(shown.text);
|
|
108
|
+
}
|
|
109
|
+
/** Part of the structured output's JSON, as the model writes it. */
|
|
110
|
+
async *draft(json) {
|
|
111
|
+
if (this.generation.stream === false)
|
|
112
|
+
return;
|
|
113
|
+
const text = await this.drafts.read(json);
|
|
106
114
|
if (text)
|
|
107
|
-
yield {
|
|
115
|
+
yield { type: 'structured_draft', text };
|
|
108
116
|
}
|
|
109
117
|
/** The drafts' tail the gate still holds, once the step's stream has ended. */
|
|
110
118
|
*end() {
|
|
@@ -131,11 +139,13 @@ class ToolDrafts {
|
|
|
131
139
|
policy;
|
|
132
140
|
context;
|
|
133
141
|
tools;
|
|
142
|
+
answerTool;
|
|
134
143
|
drafts = new Map();
|
|
135
|
-
constructor(policy, context, tools) {
|
|
144
|
+
constructor(policy, context, tools, answerTool) {
|
|
136
145
|
this.policy = policy;
|
|
137
146
|
this.context = context;
|
|
138
147
|
this.tools = tools;
|
|
148
|
+
this.answerTool = answerTool;
|
|
139
149
|
}
|
|
140
150
|
/**
|
|
141
151
|
* Shows `event` when it is a draft and says whether it was; any other event closes its call's
|
|
@@ -178,7 +188,9 @@ class ToolDrafts {
|
|
|
178
188
|
this.drafts.delete(event.tool.callId);
|
|
179
189
|
}
|
|
180
190
|
async open(event) {
|
|
181
|
-
|
|
191
|
+
// why: The answer's drafts are the structured output's, read at `reply_structured` by `StructuredDrafts`.
|
|
192
|
+
const answer = this.answerTool && event.name === ANSWER_TOOL;
|
|
193
|
+
const boundary = answer ? undefined : toolArgumentsBoundary(this.tools, event.name);
|
|
182
194
|
const gate = boundary ? await createDraftGate(this.policy, this.context, boundary) : null;
|
|
183
195
|
const draft = { name: event.name, gate };
|
|
184
196
|
this.drafts.set(event.callId, draft);
|
|
@@ -216,7 +228,7 @@ async function* yieldProviderEvents(args) {
|
|
|
216
228
|
const { canaryCarry, thoughtCarry } = control ?? {};
|
|
217
229
|
const gate = await createOutboundProgressiveGate(policy, context, 'reply', canaryCarry);
|
|
218
230
|
const thoughts = await thoughtGuardFor(policy, context, thoughtCarry);
|
|
219
|
-
const drafts = new ToolDrafts(policy, context, args.tools);
|
|
231
|
+
const drafts = new ToolDrafts(policy, context, args.tools, args.answerTool === true);
|
|
220
232
|
/** The streamed event whose reply sits in the gate's lookback; released tails keep its shape. */
|
|
221
233
|
let pendingStream = null;
|
|
222
234
|
let withholdVisible = false;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errorKind } from '../../guardrails/error.js';
|
|
1
|
+
import { describeError, errorKind } from '../../guardrails/error.js';
|
|
2
2
|
import { traceBytes, traceContent, traceJson, } from '../../observability/trace-span.js';
|
|
3
3
|
import { historyMessageParts } from '../interaction-parts.js';
|
|
4
4
|
import { isFailedStopKind } from '../stop.js';
|
|
@@ -597,7 +597,7 @@ function errorName(err) {
|
|
|
597
597
|
function recordException(span, err) {
|
|
598
598
|
span.event('exception', {
|
|
599
599
|
'exception.type': errorName(err),
|
|
600
|
-
'exception.message': traceContent(
|
|
600
|
+
'exception.message': traceContent(describeError(err)),
|
|
601
601
|
...(err instanceof Error && err.stack
|
|
602
602
|
? { 'exception.stacktrace': traceContent(err.stack) }
|
|
603
603
|
: {}),
|