@namzu/sdk 23.0.0 → 26.0.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/CHANGELOG.md +208 -0
- package/README.md +1 -1
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +3 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/connector/mcp/stdio.d.ts +11 -0
- package/dist/connector/mcp/stdio.d.ts.map +1 -1
- package/dist/connector/mcp/stdio.js +106 -1
- package/dist/connector/mcp/stdio.js.map +1 -1
- package/dist/constants/telemetry/index.d.ts +15 -1
- package/dist/constants/telemetry/index.d.ts.map +1 -1
- package/dist/constants/telemetry/index.js +15 -1
- package/dist/constants/telemetry/index.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +8 -2
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/retry.d.ts +10 -5
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +11 -31
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +12 -0
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +34 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +61 -0
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +31 -13
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +28 -6
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +178 -20
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts +14 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +19 -3
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +10 -1
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/tool-pause.d.ts +35 -0
- package/dist/runtime/query/tool-pause.d.ts.map +1 -1
- package/dist/runtime/query/tool-pause.js +35 -0
- package/dist/runtime/query/tool-pause.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +3 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/types/agent/reactive.d.ts +1 -0
- package/dist/types/agent/reactive.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +17 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +23 -2
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/run/step.d.ts +75 -3
- package/dist/types/run/step.d.ts.map +1 -1
- package/dist/types/run/step.js.map +1 -1
- package/dist/utils/backoff.d.ts +44 -0
- package/dist/utils/backoff.d.ts.map +1 -0
- package/dist/utils/backoff.js +58 -0
- package/dist/utils/backoff.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/ReactiveAgent.ts +3 -0
- package/src/connector/mcp/stdio.ts +106 -1
- package/src/constants/telemetry/index.ts +16 -1
- package/src/provider/fallback.ts +8 -2
- package/src/provider/retry.ts +20 -37
- package/src/public-runtime.ts +5 -0
- package/src/registry/tool/execute.ts +12 -0
- package/src/runtime/query/executor.ts +74 -0
- package/src/runtime/query/index.ts +59 -18
- package/src/runtime/query/iteration/index.ts +199 -22
- package/src/runtime/query/iteration/stream-turn.ts +24 -3
- package/src/runtime/query/resume-pending.ts +10 -1
- package/src/runtime/query/tool-pause.ts +37 -0
- package/src/runtime/query/tooling.ts +5 -0
- package/src/types/agent/reactive.ts +1 -0
- package/src/types/connector/mcp.ts +17 -0
- package/src/types/provider/interface.ts +23 -2
- package/src/types/run/step.ts +76 -3
- package/src/utils/backoff.ts +70 -0
|
@@ -32,6 +32,7 @@ import type { AnswerReview } from '../../../types/run/answer-review.js'
|
|
|
32
32
|
import type {
|
|
33
33
|
PrepareStepResult,
|
|
34
34
|
RunEvent,
|
|
35
|
+
StepFailure,
|
|
35
36
|
StepProvenance,
|
|
36
37
|
StepResult,
|
|
37
38
|
StopReason,
|
|
@@ -258,6 +259,30 @@ export class IterationOrchestrator {
|
|
|
258
259
|
{},
|
|
259
260
|
parentContext(this.ctx.rootSpan),
|
|
260
261
|
)
|
|
262
|
+
|
|
263
|
+
// Everything the step record needs, hoisted so the `catch` can
|
|
264
|
+
// read whatever the iteration got as far as computing.
|
|
265
|
+
//
|
|
266
|
+
// The failure path is the one the ledger's own argument was
|
|
267
|
+
// written for and the one it never reached: an iteration that
|
|
268
|
+
// threw recorded a span exception and re-threw, so the turn with
|
|
269
|
+
// no record was exactly the turn that went wrong. A reader could
|
|
270
|
+
// not tell that from a turn that never happened.
|
|
271
|
+
//
|
|
272
|
+
// Declared as `let` with real initial values rather than left
|
|
273
|
+
// undefined, because a failure BEFORE the snapshot below is
|
|
274
|
+
// taken has spent nothing, and these are then exact. The success
|
|
275
|
+
// path is untouched: the assignments inside the try still happen
|
|
276
|
+
// where they always did, so compaction and the working-memory
|
|
277
|
+
// refresh stay outside a successful step's window.
|
|
278
|
+
let stepStartedAt = Date.now()
|
|
279
|
+
let usageBefore: TokenUsage = { ...runMgr.tokenUsage }
|
|
280
|
+
let costBefore: CostInfo = { ...runMgr.costInfo }
|
|
281
|
+
let stepModel = model
|
|
282
|
+
let stepMessageId: MessageId | undefined
|
|
283
|
+
let stepResponse: ChatCompletionResponse | undefined
|
|
284
|
+
let stepServedBy: StepProvenance | undefined
|
|
285
|
+
|
|
261
286
|
try {
|
|
262
287
|
// Tool spans for this turn belong under this iteration. Inside
|
|
263
288
|
// the try rather than before it: a throw from any of these left
|
|
@@ -303,9 +328,9 @@ export class IterationOrchestrator {
|
|
|
303
328
|
// tool_use/tool_result blocks.
|
|
304
329
|
// Snapshot the cumulative counters so the step can report ITS
|
|
305
330
|
// own usage rather than the run total.
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
331
|
+
stepStartedAt = Date.now()
|
|
332
|
+
usageBefore = { ...runMgr.tokenUsage }
|
|
333
|
+
costBefore = { ...runMgr.costInfo }
|
|
309
334
|
|
|
310
335
|
// Shape this step before calling the model. `stopWhen` decides
|
|
311
336
|
// whether to keep going; this decides HOW. No-op when the host
|
|
@@ -320,7 +345,7 @@ export class IterationOrchestrator {
|
|
|
320
345
|
// still call any of them by name.
|
|
321
346
|
this.ctx.toolExecutor.setStepAllowedTools(stepAllowedTools)
|
|
322
347
|
const enforceToolInputSchema = enforcedModelInputToolNames(this.ctx.tools, llmTools)
|
|
323
|
-
|
|
348
|
+
stepModel = step.model ?? model
|
|
324
349
|
|
|
325
350
|
const baseMessages = forceFinalize
|
|
326
351
|
? [
|
|
@@ -389,6 +414,16 @@ export class IterationOrchestrator {
|
|
|
389
414
|
// aggregated `ChatCompletionResponse` for the legacy
|
|
390
415
|
// downstream paths (assistantMsg construction, working
|
|
391
416
|
// state extraction, telemetry attribute stamping).
|
|
417
|
+
//
|
|
418
|
+
// The message id is minted HERE, immediately before the call
|
|
419
|
+
// that announces it,
|
|
420
|
+
// rather than inside that call. The return value never arrives
|
|
421
|
+
// when the stream throws, so a step recorded from the catch
|
|
422
|
+
// could otherwise never name the message — and a stream that
|
|
423
|
+
// died part-way has already emitted both `message_started` and
|
|
424
|
+
// `message_completed` under this id, which is the trail a
|
|
425
|
+
// reader wants most on exactly that turn.
|
|
426
|
+
stepMessageId = generateMessageId()
|
|
392
427
|
const { response, messageId } = yield* streamProviderTurn(
|
|
393
428
|
this.ctx.provider,
|
|
394
429
|
{
|
|
@@ -424,7 +459,9 @@ export class IterationOrchestrator {
|
|
|
424
459
|
forceFinalize,
|
|
425
460
|
this.ctx.log,
|
|
426
461
|
iterSpan,
|
|
462
|
+
stepMessageId,
|
|
427
463
|
)
|
|
464
|
+
stepResponse = response
|
|
428
465
|
|
|
429
466
|
// Who answered THIS turn.
|
|
430
467
|
//
|
|
@@ -459,6 +496,7 @@ export class IterationOrchestrator {
|
|
|
459
496
|
chainIndex: member.index,
|
|
460
497
|
}
|
|
461
498
|
})()
|
|
499
|
+
stepServedBy = servedBy
|
|
462
500
|
|
|
463
501
|
// Main-loop turn: also records the prompt size compaction reads.
|
|
464
502
|
//
|
|
@@ -1014,6 +1052,76 @@ export class IterationOrchestrator {
|
|
|
1014
1052
|
})
|
|
1015
1053
|
yield* this.ctx.drainPending()
|
|
1016
1054
|
} catch (err) {
|
|
1055
|
+
const cancelled = this.ctx.abortController.signal.aborted
|
|
1056
|
+
|
|
1057
|
+
// This iteration gets a step too, and it is the one the
|
|
1058
|
+
// argument three hundred lines above was actually about.
|
|
1059
|
+
//
|
|
1060
|
+
// That docblock makes the case for a rejected tool batch — "a
|
|
1061
|
+
// run that spent a turn getting its tools refused still spent
|
|
1062
|
+
// the tokens" — and every call site it produced sat on a
|
|
1063
|
+
// success path. So the ledger was complete except on the turns
|
|
1064
|
+
// that failed, which is the worst shape it could have: an
|
|
1065
|
+
// evidence record that goes quiet exactly where something went
|
|
1066
|
+
// wrong reads as "nothing went wrong". A reader could not
|
|
1067
|
+
// distinguish iteration N failing from iteration N never
|
|
1068
|
+
// happening, while the events said plainly that it started.
|
|
1069
|
+
//
|
|
1070
|
+
// Recorded HERE, at the top of the catch, rather than at each
|
|
1071
|
+
// of its exits — the same reasoning the success path already
|
|
1072
|
+
// wrote down for itself. All three exits spend a turn: the
|
|
1073
|
+
// cancellation breaks, the overflow-relief retry continues
|
|
1074
|
+
// under a NEW iteration number (so its tokens belong to no
|
|
1075
|
+
// later step), and the re-throw ends the run.
|
|
1076
|
+
//
|
|
1077
|
+
// What it carries is what the iteration got as far as knowing.
|
|
1078
|
+
// `usage` is the same subtraction a successful step makes, so
|
|
1079
|
+
// a turn that failed after the provider answered carries that
|
|
1080
|
+
// answer's tokens, and one that failed before it carries the
|
|
1081
|
+
// zero it actually spent. Nothing is estimated to fill a gap.
|
|
1082
|
+
//
|
|
1083
|
+
// At most ONE step per iteration. Both success paths record
|
|
1084
|
+
// before the work that follows them — the advisory phase, the
|
|
1085
|
+
// structured-output capture, the `iteration_end` hooks, the
|
|
1086
|
+
// terminal `iteration_completed` — and any of those can throw
|
|
1087
|
+
// into here. A second entry numbered N would double-count that
|
|
1088
|
+
// turn's tokens against `run.tokenUsage`, which is the same
|
|
1089
|
+
// class of wrong as dropping them and harder to notice, since
|
|
1090
|
+
// the ledger would look fuller rather than emptier. That turn's
|
|
1091
|
+
// own verdict is already written down; the failure that
|
|
1092
|
+
// followed it reaches the caller as the run's error.
|
|
1093
|
+
if (this.steps.at(-1)?.stepNumber === iterationNum) {
|
|
1094
|
+
this.ctx.log.warn('Iteration failed after its step was already recorded', {
|
|
1095
|
+
runId: runMgr.id,
|
|
1096
|
+
iteration: iterationNum,
|
|
1097
|
+
error: toErrorMessage(err),
|
|
1098
|
+
})
|
|
1099
|
+
} else {
|
|
1100
|
+
this.recordStep({
|
|
1101
|
+
stepNumber: iterationNum,
|
|
1102
|
+
model: stepModel,
|
|
1103
|
+
...(stepServedBy ? { servedBy: stepServedBy } : {}),
|
|
1104
|
+
...(stepMessageId ? { messageId: stepMessageId } : {}),
|
|
1105
|
+
...(stepResponse ? { response: stepResponse } : {}),
|
|
1106
|
+
// Tool outcomes are produced and returned together by
|
|
1107
|
+
// `runToolReview`, so a throw from inside it leaves none
|
|
1108
|
+
// to salvage: an empty list here means "none came back",
|
|
1109
|
+
// which is what the shorter-than-`toolCalls` contract
|
|
1110
|
+
// says.
|
|
1111
|
+
toolResults: [],
|
|
1112
|
+
toolExecutionMs: 0,
|
|
1113
|
+
startedAt: stepStartedAt,
|
|
1114
|
+
usageBefore,
|
|
1115
|
+
costBefore,
|
|
1116
|
+
unfinished: cancelled
|
|
1117
|
+
? { finishReason: 'cancelled' }
|
|
1118
|
+
: {
|
|
1119
|
+
finishReason: 'error',
|
|
1120
|
+
failure: describeStepFailure(err, this.ctx.provider.id),
|
|
1121
|
+
},
|
|
1122
|
+
})
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1017
1125
|
// A Stop that aborted the in-flight turn surfaces here as a
|
|
1018
1126
|
// thrown abort (the provider stream was raced against the run
|
|
1019
1127
|
// signal). Settle it as a CANCELLATION — mirroring the
|
|
@@ -1021,7 +1129,7 @@ export class IterationOrchestrator {
|
|
|
1021
1129
|
// recording it as an SDK failure (error span + failed activity)
|
|
1022
1130
|
// and re-throwing. The run then returns cleanly with a
|
|
1023
1131
|
// 'cancelled' stop reason instead of propagating an error.
|
|
1024
|
-
if (
|
|
1132
|
+
if (cancelled) {
|
|
1025
1133
|
runMgr.setStopReason('cancelled')
|
|
1026
1134
|
runMgr.markCancelled()
|
|
1027
1135
|
break
|
|
@@ -1308,39 +1416,69 @@ export class IterationOrchestrator {
|
|
|
1308
1416
|
private recordStep(input: {
|
|
1309
1417
|
stepNumber: number
|
|
1310
1418
|
model: string
|
|
1311
|
-
servedBy
|
|
1312
|
-
messageId
|
|
1313
|
-
|
|
1419
|
+
servedBy?: StepProvenance
|
|
1420
|
+
messageId?: MessageId
|
|
1421
|
+
/**
|
|
1422
|
+
* The turn's response. Absent only when the iteration failed before
|
|
1423
|
+
* the provider produced one — see `unfinished`.
|
|
1424
|
+
*/
|
|
1425
|
+
response?: ChatCompletionResponse
|
|
1314
1426
|
toolResults: readonly ToolCallOutcome[]
|
|
1315
1427
|
toolExecutionMs: number
|
|
1316
1428
|
startedAt: number
|
|
1317
1429
|
usageBefore: TokenUsage
|
|
1318
1430
|
costBefore: CostInfo
|
|
1431
|
+
/**
|
|
1432
|
+
* Set only by the `catch`, for an iteration that did not finish.
|
|
1433
|
+
*
|
|
1434
|
+
* The same writer builds both records on purpose: a failed turn's
|
|
1435
|
+
* step is a `StepResult` like any other, so a caller reconstructing
|
|
1436
|
+
* cost or history sorts them together instead of discovering that
|
|
1437
|
+
* failures live somewhere else.
|
|
1438
|
+
*/
|
|
1439
|
+
unfinished?: { finishReason: 'error' | 'cancelled'; failure?: StepFailure }
|
|
1319
1440
|
}): void {
|
|
1320
1441
|
const { runMgr } = this.ctx
|
|
1321
|
-
const toolCalls = input.response
|
|
1442
|
+
const toolCalls = input.response?.message.toolCalls ?? []
|
|
1322
1443
|
const byId = new Map(input.toolResults.map((r) => [r.toolCallId, r]))
|
|
1323
1444
|
|
|
1324
1445
|
const step: StepResult = {
|
|
1325
1446
|
stepNumber: input.stepNumber,
|
|
1326
1447
|
model: input.model,
|
|
1327
|
-
servedBy: input.servedBy,
|
|
1328
|
-
messageId: input.messageId,
|
|
1329
|
-
content: input.response
|
|
1448
|
+
...(input.servedBy ? { servedBy: input.servedBy } : {}),
|
|
1449
|
+
...(input.messageId ? { messageId: input.messageId } : {}),
|
|
1450
|
+
content: input.response?.message.content ?? null,
|
|
1330
1451
|
toolCalls,
|
|
1331
1452
|
// Ordered by the tool CALLS, not by completion, so the record
|
|
1332
1453
|
// matches what the model asked for.
|
|
1333
|
-
|
|
1454
|
+
//
|
|
1455
|
+
// On an unfinished step the calls with no outcome are DROPPED
|
|
1456
|
+
// rather than filled with `{output: '', isError: false}`. That
|
|
1457
|
+
// filler is a reading of "the batch was refused" on the success
|
|
1458
|
+
// path, where every call in a batch shares one verdict; under a
|
|
1459
|
+
// step that says `error` it would say a tool ran and returned
|
|
1460
|
+
// nothing successfully, which is the same lie one level down as
|
|
1461
|
+
// the missing step itself.
|
|
1462
|
+
toolResults: toolCalls.flatMap((tc) => {
|
|
1334
1463
|
const outcome = byId.get(tc.id)
|
|
1335
|
-
return
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1464
|
+
if (input.unfinished && !outcome) return []
|
|
1465
|
+
return [
|
|
1466
|
+
{
|
|
1467
|
+
toolCallId: tc.id,
|
|
1468
|
+
toolName: tc.function.name,
|
|
1469
|
+
output: outcome?.output ?? '',
|
|
1470
|
+
isError: outcome?.isError ?? false,
|
|
1471
|
+
durationMs: 0,
|
|
1472
|
+
},
|
|
1473
|
+
]
|
|
1342
1474
|
}),
|
|
1343
|
-
|
|
1475
|
+
// The turn's own verdict where there is one. A step that ended in
|
|
1476
|
+
// the catch has none — no provider reported `error` or
|
|
1477
|
+
// `cancelled` — so `unfinished` wins even when a response had
|
|
1478
|
+
// already arrived: a turn that answered and then threw during
|
|
1479
|
+
// tool execution did not end in `tool_calls`.
|
|
1480
|
+
finishReason: input.unfinished?.finishReason ?? input.response?.finishReason ?? 'error',
|
|
1481
|
+
...(input.unfinished?.failure ? { failure: input.unfinished.failure } : {}),
|
|
1344
1482
|
usage: subtractUsage(runMgr.tokenUsage, input.usageBefore),
|
|
1345
1483
|
costDelta: {
|
|
1346
1484
|
...runMgr.costInfo,
|
|
@@ -1352,7 +1490,26 @@ export class IterationOrchestrator {
|
|
|
1352
1490
|
}
|
|
1353
1491
|
|
|
1354
1492
|
this.steps.push(step)
|
|
1355
|
-
|
|
1493
|
+
|
|
1494
|
+
if (!input.unfinished) {
|
|
1495
|
+
this.ctx.onStepFinish?.(step)
|
|
1496
|
+
return
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Nothing here is allowed to throw over the failure that is already
|
|
1500
|
+
// unwinding — the same rule `settleCancelledTurn` states for the
|
|
1501
|
+
// cancellation path. A host callback that throws while being told a
|
|
1502
|
+
// turn failed would REPLACE the reason the turn failed, so the run
|
|
1503
|
+
// would report the observer's bug and lose the original.
|
|
1504
|
+
try {
|
|
1505
|
+
this.ctx.onStepFinish?.(step)
|
|
1506
|
+
} catch (err) {
|
|
1507
|
+
this.ctx.log.warn('onStepFinish threw while recording a failed step', {
|
|
1508
|
+
runId: runMgr.id,
|
|
1509
|
+
step: input.stepNumber,
|
|
1510
|
+
error: toErrorMessage(err),
|
|
1511
|
+
})
|
|
1512
|
+
}
|
|
1356
1513
|
}
|
|
1357
1514
|
|
|
1358
1515
|
/** Turns spent asking the model again for a valid structured output. */
|
|
@@ -1621,6 +1778,26 @@ export class IterationOrchestrator {
|
|
|
1621
1778
|
}
|
|
1622
1779
|
}
|
|
1623
1780
|
|
|
1781
|
+
/**
|
|
1782
|
+
* Fold whatever ended an iteration into the record a reader gets.
|
|
1783
|
+
*
|
|
1784
|
+
* Classified through `classifyProviderError` — the same call the catch
|
|
1785
|
+
* already makes to decide whether compaction relief applies — so the step's
|
|
1786
|
+
* verdict and the loop's own decision cannot drift apart. It also handles a
|
|
1787
|
+
* failure that is not a provider failure at all: the code set's `unknown`
|
|
1788
|
+
* means "unclassifiable", which is the true answer for a plugin hook that
|
|
1789
|
+
* threw and is left saying so rather than dressed up as something specific.
|
|
1790
|
+
*/
|
|
1791
|
+
function describeStepFailure(err: unknown, providerId: string): StepFailure {
|
|
1792
|
+
const classified = classifyProviderError(err, providerId)
|
|
1793
|
+
return {
|
|
1794
|
+
message: toErrorMessage(err),
|
|
1795
|
+
code: classified.code,
|
|
1796
|
+
...(classified.status !== undefined ? { status: classified.status } : {}),
|
|
1797
|
+
retryable: classified.retryable,
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1624
1801
|
/** Per-step usage: the delta between two cumulative snapshots. */
|
|
1625
1802
|
function subtractUsage(after: TokenUsage, before: TokenUsage): TokenUsage {
|
|
1626
1803
|
return {
|
|
@@ -134,6 +134,19 @@ export async function* streamProviderTurn(
|
|
|
134
134
|
forceFinalize: boolean,
|
|
135
135
|
log: Logger,
|
|
136
136
|
parentSpan?: Span,
|
|
137
|
+
/**
|
|
138
|
+
* The id to announce this message under.
|
|
139
|
+
*
|
|
140
|
+
* Supplied by the loop so a turn that THROWS still leaves the caller
|
|
141
|
+
* holding the id it announced. The return value never arrives on a
|
|
142
|
+
* failure, so without this the one case where a failed step most wants
|
|
143
|
+
* to point at the event stream — a stream that died after
|
|
144
|
+
* `message_started`, having already emitted `message_completed` on the
|
|
145
|
+
* way out — is precisely the case that could not.
|
|
146
|
+
*
|
|
147
|
+
* Optional, so a caller with no use for the id is unchanged.
|
|
148
|
+
*/
|
|
149
|
+
announceAs?: import('../../../types/ids/index.js').MessageId,
|
|
137
150
|
): AsyncGenerator<RunEvent, StreamingTurnResult> {
|
|
138
151
|
// The `chat {model}` span the GenAI conventions require. There was none:
|
|
139
152
|
// `chatSpanName` existed with zero call sites, so a trace carried no LLM
|
|
@@ -151,7 +164,7 @@ export async function* streamProviderTurn(
|
|
|
151
164
|
...(params.maxTokens !== undefined ? { [GENAI.REQUEST_MAX_TOKENS]: params.maxTokens } : {}),
|
|
152
165
|
})
|
|
153
166
|
|
|
154
|
-
const messageId = generateMessageId()
|
|
167
|
+
const messageId = announceAs ?? generateMessageId()
|
|
155
168
|
await emitEvent({ type: 'message_started', runId, iteration, messageId })
|
|
156
169
|
yield* drainPending()
|
|
157
170
|
|
|
@@ -194,7 +207,12 @@ export async function* streamProviderTurn(
|
|
|
194
207
|
// verbatim — so the map is drained in index order at the end.
|
|
195
208
|
const reasoningBuckets = new Map<
|
|
196
209
|
number,
|
|
197
|
-
{
|
|
210
|
+
{
|
|
211
|
+
type: 'thinking' | 'redacted_thinking'
|
|
212
|
+
text: string
|
|
213
|
+
signature?: string
|
|
214
|
+
encrypted?: string
|
|
215
|
+
}
|
|
198
216
|
>()
|
|
199
217
|
|
|
200
218
|
// Citations arrive as their own deltas, in the order the model made
|
|
@@ -205,7 +223,10 @@ export async function* streamProviderTurn(
|
|
|
205
223
|
let streamError: string | undefined
|
|
206
224
|
let streamCause: unknown
|
|
207
225
|
|
|
208
|
-
const stream = provider.chatStream({
|
|
226
|
+
const stream = provider.chatStream({
|
|
227
|
+
...params,
|
|
228
|
+
stream: true,
|
|
229
|
+
}) as AsyncIterable<StreamChunk>
|
|
209
230
|
|
|
210
231
|
// Drive the stream manually so each `.next()` can be RACED against the run
|
|
211
232
|
// abort: a Stop tears the in-flight model request down (the provider got
|
|
@@ -9,6 +9,7 @@ import type { ChatCompletionResponse } from '../../types/provider/index.js'
|
|
|
9
9
|
import type { Logger } from '../../utils/logger.js'
|
|
10
10
|
import type { PriorToolResults, ToolCallDenials, ToolExecutor } from './executor.js'
|
|
11
11
|
import { PendingAnswers } from './question-park.js'
|
|
12
|
+
import { isPauseForCall } from './tool-pause.js'
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Apply a decision collected out-of-band to the tool calls a run parked on.
|
|
@@ -154,7 +155,15 @@ function planQuestionResume(
|
|
|
154
155
|
// otherwise have its answer delivered to whatever tool now holds that
|
|
155
156
|
// slot — the misdirection the asking tool's own id guard exists to
|
|
156
157
|
// prevent, checked here too because by then the tool has been entered.
|
|
157
|
-
|
|
158
|
+
//
|
|
159
|
+
// Through `isPauseForCall` rather than by equality, because a parked
|
|
160
|
+
// question id is not a call id. The general seam appends the tool
|
|
161
|
+
// author's pause name to it, so equality compared
|
|
162
|
+
// `call_1:target_environment` against `call_1`, could never hold, and
|
|
163
|
+
// refused every cross-process resume of a host-authored pause. Only
|
|
164
|
+
// the built-in question tool got through, and only because it parks
|
|
165
|
+
// under the bare tool-use id.
|
|
166
|
+
if (!assistant.toolCalls.some((tc) => isPauseForCall(questionId, tc.id))) {
|
|
158
167
|
log.error('The parked question does not belong to any unanswered call in this turn', {
|
|
159
168
|
checkpointId: checkpoint.id,
|
|
160
169
|
questionId,
|
|
@@ -32,6 +32,43 @@ import type { PendingAnswers, QuestionParkRecorder } from './question-park.js'
|
|
|
32
32
|
*/
|
|
33
33
|
export const pauseId = (toolUseId: string, name: string): string => `${toolUseId}:${name}`
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Does this pause id belong to `callId`?
|
|
37
|
+
*
|
|
38
|
+
* The other half of the scheme above, and it lives beside the mint rather
|
|
39
|
+
* than at the gate that asks it, so the two cannot drift apart again. They
|
|
40
|
+
* already had: the resume gate compared the whole parked id against a raw
|
|
41
|
+
* tool-use id, which a composite can never equal, so a pause raised
|
|
42
|
+
* through {@link createToolPause} was refused by every cross-process
|
|
43
|
+
* resume — while the built-in question tool, whose `questionId` IS the raw
|
|
44
|
+
* id, passed the same gate and worked.
|
|
45
|
+
*
|
|
46
|
+
* Asked against an id actually present in the turn rather than by
|
|
47
|
+
* splitting the composite on `:`. The left half is the provider's tool-use
|
|
48
|
+
* id and nothing forbids a colon in it, so a split takes `call:9:confirm`
|
|
49
|
+
* apart at the wrong place and then compares against `call` — a string no
|
|
50
|
+
* provider minted. Testing a `<callId>:` prefix asks about candidates that
|
|
51
|
+
* exist instead.
|
|
52
|
+
*
|
|
53
|
+
* It is not exact, and the inexactness is bounded rather than absent: a
|
|
54
|
+
* call whose id is literally `call` matches a pause raised on `call:9`.
|
|
55
|
+
* That costs nothing while the real call is also in the turn, because the
|
|
56
|
+
* pause does belong to a call there; and when it is not, the answer is
|
|
57
|
+
* filed under a key no tool asks for, so the tool asks again. A resume
|
|
58
|
+
* that re-asks, never one that misdelivers — routing below is still an
|
|
59
|
+
* exact key match.
|
|
60
|
+
*
|
|
61
|
+
* Membership only — is there a call in this turn that this pause was
|
|
62
|
+
* raised from. The full composite still ROUTES the answer, by exact key in
|
|
63
|
+
* `PendingAnswers`, which is what keeps a call that pauses twice from
|
|
64
|
+
* delivering its second answer against its first question.
|
|
65
|
+
*
|
|
66
|
+
* Every sentence above that names a condition and an outcome is pinned in
|
|
67
|
+
* `__tests__/tool-pause.test.ts`, under "the id a resume gate matches on".
|
|
68
|
+
*/
|
|
69
|
+
export const isPauseForCall = (pause: string, callId: string): boolean =>
|
|
70
|
+
pause === callId || pause.startsWith(`${callId}:`)
|
|
71
|
+
|
|
35
72
|
interface ToolPauseDeps {
|
|
36
73
|
readonly runId: RunId
|
|
37
74
|
readonly toolUseId: string
|
|
@@ -6,6 +6,7 @@ import type { PermissionMode } from '../../types/permission/index.js'
|
|
|
6
6
|
import type { RunEvent } from '../../types/run/index.js'
|
|
7
7
|
import type { RequestToolPause, ToolRegistryContract } from '../../types/tool/index.js'
|
|
8
8
|
import type { RepairToolCall } from '../../types/tool/repair.js'
|
|
9
|
+
import type { BackoffPolicy } from '../../utils/backoff.js'
|
|
9
10
|
import type { Logger } from '../../utils/logger.js'
|
|
10
11
|
import { ToolExecutor } from './executor.js'
|
|
11
12
|
|
|
@@ -22,6 +23,7 @@ export interface ToolingBootstrapConfig {
|
|
|
22
23
|
invocationState?: InvocationState
|
|
23
24
|
pluginManager?: PluginLifecycleManager
|
|
24
25
|
toolTimeoutMs?: number
|
|
26
|
+
toolRetryBackoff?: Partial<BackoffPolicy>
|
|
25
27
|
maxToolConcurrency?: number
|
|
26
28
|
maxToolOutputChars?: number
|
|
27
29
|
maxToolContentBytes?: number
|
|
@@ -50,6 +52,9 @@ export class ToolingBootstrap {
|
|
|
50
52
|
invocationState: config.invocationState,
|
|
51
53
|
pluginManager: config.pluginManager,
|
|
52
54
|
...(config.toolTimeoutMs !== undefined ? { toolTimeoutMs: config.toolTimeoutMs } : {}),
|
|
55
|
+
...(config.toolRetryBackoff !== undefined
|
|
56
|
+
? { toolRetryBackoff: config.toolRetryBackoff }
|
|
57
|
+
: {}),
|
|
53
58
|
...(config.maxToolConcurrency !== undefined
|
|
54
59
|
? { maxToolConcurrency: config.maxToolConcurrency }
|
|
55
60
|
: {}),
|
|
@@ -86,6 +86,7 @@ export interface ReactiveAgentConfig extends BaseAgentConfig {
|
|
|
86
86
|
retry?: QueryParams['retry']
|
|
87
87
|
emergencySave?: boolean
|
|
88
88
|
toolTimeoutMs?: number
|
|
89
|
+
toolRetryBackoff?: QueryParams['toolRetryBackoff']
|
|
89
90
|
maxToolConcurrency?: number
|
|
90
91
|
maxToolOutputChars?: number
|
|
91
92
|
/**
|
|
@@ -18,7 +18,24 @@ export interface MCPStdioTransportConfig extends MCPTransportConfigBase {
|
|
|
18
18
|
type: 'stdio'
|
|
19
19
|
command: string
|
|
20
20
|
args?: string[]
|
|
21
|
+
/** Literal values for the child. Highest precedence. */
|
|
21
22
|
env?: Record<string, string>
|
|
23
|
+
/**
|
|
24
|
+
* Parent variables the child may have, named one at a time.
|
|
25
|
+
*
|
|
26
|
+
* The spawn used to pass the whole parent environment, so a server that
|
|
27
|
+
* needed one token received every credential the host held. It now gets
|
|
28
|
+
* process plumbing plus what is named here and in `env`, which is what
|
|
29
|
+
* makes the grant reviewable: the config says which secrets cross the
|
|
30
|
+
* boundary instead of the answer being "all of them".
|
|
31
|
+
*
|
|
32
|
+
* Use this rather than `env` for a credential — `env` puts the value in the
|
|
33
|
+
* config file, and this keeps it in the environment where it already lives.
|
|
34
|
+
*
|
|
35
|
+
* A name the parent does not hold is absent from the child rather than
|
|
36
|
+
* empty, and does not fail the spawn.
|
|
37
|
+
*/
|
|
38
|
+
inheritEnv?: readonly string[]
|
|
22
39
|
cwd?: string
|
|
23
40
|
}
|
|
24
41
|
|
|
@@ -73,7 +73,23 @@ export interface LLMProvider {
|
|
|
73
73
|
*/
|
|
74
74
|
probeCredential?(): Promise<void>
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Is this driver able to serve traffic? A summary bit; `doctorCheck` is
|
|
78
|
+
* the same probe with its reasoning intact.
|
|
79
|
+
*
|
|
80
|
+
* `model` is the model the CALLER intends to run, and it is a parameter
|
|
81
|
+
* rather than something the driver reads off its own config because at
|
|
82
|
+
* least one driver's config does not carry a model at all. That driver
|
|
83
|
+
* hardcoded an id instead, which is how its check came to probe a model
|
|
84
|
+
* nobody used — and, once the id went stale, could not pass at any
|
|
85
|
+
* credential, region or service state. A health check against a model the
|
|
86
|
+
* operator does not run tests the wrong thing even while the id is valid.
|
|
87
|
+
*
|
|
88
|
+
* Optional, and a driver may ignore it: one that probes an endpoint rather
|
|
89
|
+
* than a model has nothing to do with the argument. Passing it is always
|
|
90
|
+
* safe.
|
|
91
|
+
*/
|
|
92
|
+
healthCheck?(model?: string): Promise<boolean>
|
|
77
93
|
|
|
78
94
|
/**
|
|
79
95
|
* Optional structured health probe used by `runDoctor()`.
|
|
@@ -82,8 +98,13 @@ export interface LLMProvider {
|
|
|
82
98
|
* (latency, model availability, auth status, …). Providers that
|
|
83
99
|
* cannot be cheaply probed should return `{ status: 'inconclusive' }`
|
|
84
100
|
* so the doctor doesn't mark them as failing — see ses_007 Q6.4.
|
|
101
|
+
*
|
|
102
|
+
* Takes `model` for the reason `healthCheck` does, and a driver is free to
|
|
103
|
+
* return a SUBTYPE of `DoctorCheckResult` carrying its own machine-readable
|
|
104
|
+
* detail. `status` is what `runDoctor()` reads; a caller holding the
|
|
105
|
+
* concrete driver can read more.
|
|
85
106
|
*/
|
|
86
|
-
doctorCheck?(): Promise<DoctorCheckResult>
|
|
107
|
+
doctorCheck?(model?: string): Promise<DoctorCheckResult>
|
|
87
108
|
|
|
88
109
|
/**
|
|
89
110
|
* Which {@link ChatCompletionParams.effort} levels this model accepts,
|
package/src/types/run/step.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CostInfo, TokenUsage } from '../common/index.js'
|
|
2
2
|
import type { MessageId } from '../ids/index.js'
|
|
3
3
|
import type { ToolCall } from '../message/index.js'
|
|
4
|
+
import type { ProviderErrorCode } from '../provider/errors.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* What one iteration of the agent loop did.
|
|
@@ -55,13 +56,56 @@ export interface StepResult {
|
|
|
55
56
|
* it is handed.
|
|
56
57
|
*/
|
|
57
58
|
servedBy?: StepProvenance
|
|
58
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The assistant message this step produced.
|
|
61
|
+
*
|
|
62
|
+
* Absent only on a step whose iteration ended before the model's message
|
|
63
|
+
* was announced — a compaction failure, a lifecycle hook that threw, a
|
|
64
|
+
* transport error raised before the first chunk. Absence is left meaning
|
|
65
|
+
* "there was no message" rather than filled with an id the event stream
|
|
66
|
+
* never carried: a `messageId` a reader cannot find a `message_started`
|
|
67
|
+
* for is worse evidence than no id at all, because it invites the
|
|
68
|
+
* correlation and then loses it.
|
|
69
|
+
*
|
|
70
|
+
* Present, and correlatable, on every step whose iteration got as far as
|
|
71
|
+
* the provider call — including one that failed mid-stream. The loop
|
|
72
|
+
* mints the id before the call that announces it, and a dying stream
|
|
73
|
+
* emits `message_completed` on its way out, so the events carry both
|
|
74
|
+
* ends of the message a failed step points at.
|
|
75
|
+
*/
|
|
76
|
+
messageId?: MessageId
|
|
59
77
|
/** Assistant text for this step, if any. */
|
|
60
78
|
content: string | null
|
|
61
79
|
toolCalls: readonly ToolCall[]
|
|
62
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Tool outcomes, in the same order as `toolCalls`.
|
|
82
|
+
*
|
|
83
|
+
* Shorter than `toolCalls` on a step that ended in failure: only the
|
|
84
|
+
* outcomes that exist are recorded, so a call with no entry here reads
|
|
85
|
+
* as "never came back" rather than as an empty success. Pair by
|
|
86
|
+
* `toolCallId`, not by index.
|
|
87
|
+
*/
|
|
63
88
|
toolResults: readonly StepToolResult[]
|
|
64
|
-
|
|
89
|
+
/**
|
|
90
|
+
* How the turn ended.
|
|
91
|
+
*
|
|
92
|
+
* `error` and `cancelled` are not provider verdicts — no provider reports
|
|
93
|
+
* them — but a step exists for a failed iteration too, and it has to say
|
|
94
|
+
* how it ended in the same field a reader already sorts by. `error` comes
|
|
95
|
+
* with {@link failure}; `cancelled` means a Stop tore the turn down and
|
|
96
|
+
* there is no failure to report.
|
|
97
|
+
*/
|
|
98
|
+
finishReason: 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'error' | 'cancelled'
|
|
99
|
+
/**
|
|
100
|
+
* What went wrong, on a step with `finishReason: 'error'`.
|
|
101
|
+
*
|
|
102
|
+
* Absent everywhere else. A run whose ledger is complete except on the
|
|
103
|
+
* turns that failed reads as "nothing went wrong" precisely when
|
|
104
|
+
* something did, which is worse than an absent record — so the failed
|
|
105
|
+
* turn gets the same record as every other, and this is what makes it
|
|
106
|
+
* legible as a failure.
|
|
107
|
+
*/
|
|
108
|
+
failure?: StepFailure
|
|
65
109
|
/** Usage for THIS step, not the run's cumulative total. */
|
|
66
110
|
usage: TokenUsage
|
|
67
111
|
/** Cost delta attributable to this step. Zero without a pricing table. */
|
|
@@ -88,6 +132,35 @@ export interface StepProvenance {
|
|
|
88
132
|
readonly chainIndex: number
|
|
89
133
|
}
|
|
90
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Why a step ended in `finishReason: 'error'`.
|
|
137
|
+
*
|
|
138
|
+
* The step-level counterpart of the pair a failed run already carries —
|
|
139
|
+
* {@link import('./entity.js').Run.lastError} and
|
|
140
|
+
* {@link import('./entity.js').Run.lastProviderError} — and shaped from the
|
|
141
|
+
* same classification, so the two agree when the failed step is the one that
|
|
142
|
+
* ended the run. What a run records once, a run of twenty iterations records
|
|
143
|
+
* per iteration, which is the difference between "this run failed" and "this
|
|
144
|
+
* turn failed, and the next four succeeded".
|
|
145
|
+
*/
|
|
146
|
+
export interface StepFailure {
|
|
147
|
+
/** The failure's message, as the iteration's span and log recorded it. */
|
|
148
|
+
readonly message: string
|
|
149
|
+
/**
|
|
150
|
+
* Where the classifier placed it.
|
|
151
|
+
*
|
|
152
|
+
* `unknown` for a failure that is not a provider failure at all — a
|
|
153
|
+
* plugin hook that threw, a bug in a tool wrapper. That is the honest
|
|
154
|
+
* reading of the code's own contract ("unclassifiable"), and it is left
|
|
155
|
+
* saying so rather than being given a more specific-looking code.
|
|
156
|
+
*/
|
|
157
|
+
readonly code: ProviderErrorCode
|
|
158
|
+
/** HTTP status, when the failure carried one. */
|
|
159
|
+
readonly status?: number
|
|
160
|
+
/** Whether sending the same request again could have worked. */
|
|
161
|
+
readonly retryable: boolean
|
|
162
|
+
}
|
|
163
|
+
|
|
91
164
|
export interface StepToolResult {
|
|
92
165
|
toolCallId: string
|
|
93
166
|
toolName: string
|