@namzu/sdk 52.0.0 → 52.1.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 +19 -0
- package/dist/compaction/verifier.d.ts.map +1 -1
- package/dist/compaction/verifier.js +57 -17
- package/dist/compaction/verifier.js.map +1 -1
- package/dist/contracts/a2a.d.ts +2 -2
- package/dist/provider/capabilities.d.ts +9 -0
- package/dist/provider/capabilities.d.ts.map +1 -1
- package/dist/provider/capabilities.js +15 -0
- package/dist/provider/capabilities.js.map +1 -1
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +13 -0
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +10 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/image-input-refusal.d.ts +9 -0
- package/dist/provider/image-input-refusal.d.ts.map +1 -0
- package/dist/provider/image-input-refusal.js +54 -0
- package/dist/provider/image-input-refusal.js.map +1 -0
- package/dist/provider/index.d.ts +2 -1
- package/dist/provider/index.d.ts.map +1 -1
- package/dist/provider/index.js +2 -1
- package/dist/provider/index.js.map +1 -1
- package/dist/provider/retry.d.ts +7 -2
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +38 -4
- package/dist/provider/retry.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/deferred-sandbox.d.ts +54 -0
- package/dist/runtime/query/deferred-sandbox.d.ts.map +1 -0
- package/dist/runtime/query/deferred-sandbox.js +75 -0
- package/dist/runtime/query/deferred-sandbox.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +10 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +24 -3
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +93 -25
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +26 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +164 -21
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts +10 -7
- package/dist/runtime/query/iteration/phases/compaction-breaker.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction-breaker.js +10 -7
- package/dist/runtime/query/iteration/phases/compaction-breaker.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts +42 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +119 -21
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts +21 -1
- package/dist/runtime/query/iteration/provider-rejected-image.d.ts.map +1 -1
- package/dist/runtime/query/iteration/provider-rejected-image.js +53 -7
- package/dist/runtime/query/iteration/provider-rejected-image.js.map +1 -1
- package/dist/runtime/query/iteration/request-repair.d.ts +108 -0
- package/dist/runtime/query/iteration/request-repair.d.ts.map +1 -0
- package/dist/runtime/query/iteration/request-repair.js +593 -0
- package/dist/runtime/query/iteration/request-repair.js.map +1 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts +6 -0
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +2 -2
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/request-rich-content.d.ts +5 -1
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +67 -25
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +1 -1
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +12 -5
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +30 -5
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/provider/interface.d.ts +12 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +1 -1
- package/dist/types/session/config.d.ts +14 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +20 -5
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/package.json +1 -1
- package/src/compaction/verifier.ts +57 -20
- package/src/provider/capabilities.ts +17 -0
- package/src/provider/errors.ts +13 -0
- package/src/provider/fallback.ts +10 -0
- package/src/provider/idle-timeout.ts +5 -0
- package/src/provider/image-input-refusal.ts +54 -0
- package/src/provider/index.ts +2 -0
- package/src/provider/retry.ts +42 -4
- package/src/public-runtime.ts +3 -0
- package/src/runtime/query/deferred-sandbox.ts +110 -0
- package/src/runtime/query/executor.ts +25 -3
- package/src/runtime/query/index.ts +124 -49
- package/src/runtime/query/iteration/index.ts +188 -23
- package/src/runtime/query/iteration/phases/compaction-breaker.ts +10 -8
- package/src/runtime/query/iteration/phases/compaction.ts +151 -35
- package/src/runtime/query/iteration/provider-rejected-image.ts +71 -5
- package/src/runtime/query/iteration/request-repair.ts +743 -0
- package/src/runtime/query/iteration/stream-turn.ts +11 -1
- package/src/runtime/query/request-rich-content.ts +77 -27
- package/src/tools/builtins/computer-use.ts +13 -4
- package/src/types/provider/errors.ts +33 -5
- package/src/types/provider/interface.ts +13 -0
- package/src/types/provider/stream.ts +1 -1
- package/src/types/session/config.ts +14 -0
- package/src/types/session/events.ts +25 -5
|
@@ -104,6 +104,13 @@ import { AwaitedJobs } from '../jobs/awaited-jobs.js'
|
|
|
104
104
|
import type { BackgroundJobRegistry } from '../jobs/registry.js'
|
|
105
105
|
import { catchUpFromCursor, settlePreStartCancellation } from './cancelled-before-start.js'
|
|
106
106
|
import { CheckpointManager, findPendingCheckpoint } from './checkpoint.js'
|
|
107
|
+
import {
|
|
108
|
+
type DeferredSandbox,
|
|
109
|
+
type SandboxAcquireMode,
|
|
110
|
+
type SandboxAcquisitionStop,
|
|
111
|
+
SandboxAcquisitionStopped,
|
|
112
|
+
createDeferredSandbox,
|
|
113
|
+
} from './deferred-sandbox.js'
|
|
107
114
|
import type { EventTranslator } from './events.js'
|
|
108
115
|
import { finalizeTurn } from './finalize-turn.js'
|
|
109
116
|
import { GuardCoordinator } from './guard.js'
|
|
@@ -1825,6 +1832,10 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1825
1832
|
})
|
|
1826
1833
|
|
|
1827
1834
|
let sandbox: Sandbox | undefined
|
|
1835
|
+
/** Set only for `sandbox.acquire: 'concurrent' | 'on-first-use'`. */
|
|
1836
|
+
let deferredSandbox: DeferredSandbox | undefined
|
|
1837
|
+
/** How a stopped acquisition settles the turn; set where the provider is known. */
|
|
1838
|
+
let applyAcquisitionStop: (acquisition: SandboxAcquisitionStop) => void = () => {}
|
|
1828
1839
|
// Decided during checkpoint restore, executed after the sandbox
|
|
1829
1840
|
// exists — the approved tools may well need it.
|
|
1830
1841
|
let pendingResume: PendingResumePlan | null = null
|
|
@@ -2300,6 +2311,20 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
2300
2311
|
yield* eventTranslator.drainPending()
|
|
2301
2312
|
}
|
|
2302
2313
|
|
|
2314
|
+
// Checked whether or not a provider is present: a host that mistypes the
|
|
2315
|
+
// option should hear about it before it wires a provider in, not after.
|
|
2316
|
+
const acquireMode: SandboxAcquireMode = turnConfig.sandbox?.acquire ?? 'eager'
|
|
2317
|
+
if (
|
|
2318
|
+
acquireMode !== 'eager' &&
|
|
2319
|
+
acquireMode !== 'concurrent' &&
|
|
2320
|
+
acquireMode !== 'on-first-use'
|
|
2321
|
+
) {
|
|
2322
|
+
throw new NamzuError({
|
|
2323
|
+
code: 'invalid_config',
|
|
2324
|
+
message: `sandbox.acquire must be 'eager', 'concurrent' or 'on-first-use'; received ${String(acquireMode)}`,
|
|
2325
|
+
details: { acquire: String(acquireMode) },
|
|
2326
|
+
})
|
|
2327
|
+
}
|
|
2303
2328
|
// --- Sandbox lifecycle: create before iteration loop ---
|
|
2304
2329
|
if (params.sandboxProvider) {
|
|
2305
2330
|
const rootAtCwd = turnConfig.sandbox?.workspace === 'working-directory'
|
|
@@ -2331,40 +2356,60 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
2331
2356
|
},
|
|
2332
2357
|
})
|
|
2333
2358
|
}
|
|
2334
|
-
const
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
? {
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2359
|
+
const sandboxProvider = params.sandboxProvider
|
|
2360
|
+
const startAcquisition = (signal: AbortSignal) =>
|
|
2361
|
+
acquireSandbox({
|
|
2362
|
+
provider: sandboxProvider,
|
|
2363
|
+
config: {
|
|
2364
|
+
...(rootAtCwd ? { workingDirectory: ctx.cwd } : {}),
|
|
2365
|
+
...(rootAtCwd && params.additionalDirectories?.length
|
|
2366
|
+
? { additionalDirectories: params.additionalDirectories }
|
|
2367
|
+
: {}),
|
|
2368
|
+
timeoutMs: turnConfig.sandbox?.timeoutMs,
|
|
2369
|
+
memoryLimitMb: turnConfig.sandbox?.memoryLimitMb,
|
|
2370
|
+
maxProcesses: turnConfig.sandbox?.maxProcesses,
|
|
2371
|
+
},
|
|
2372
|
+
signal,
|
|
2373
|
+
timeoutMs: guard.remainingUntilTimeoutMs(),
|
|
2374
|
+
teardownTimeoutMs: sandboxTeardownTimeoutMs,
|
|
2375
|
+
onLateTeardown: (result) => {
|
|
2376
|
+
if (result.kind === 'destroyed') {
|
|
2377
|
+
ctx.log.info('Late sandbox allocation was destroyed after the turn stopped', {
|
|
2378
|
+
[NAMZU.TURN_ID]: ctx.turnId,
|
|
2379
|
+
})
|
|
2380
|
+
return
|
|
2381
|
+
}
|
|
2382
|
+
ctx.log.error('Late sandbox allocation did not stop cleanly', {
|
|
2351
2383
|
[NAMZU.TURN_ID]: ctx.turnId,
|
|
2384
|
+
...errorAttributes(result.error),
|
|
2352
2385
|
})
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2386
|
+
},
|
|
2387
|
+
})
|
|
2388
|
+
const publishSandbox = async (acquired: Sandbox): Promise<void> => {
|
|
2389
|
+
sandbox = acquired
|
|
2390
|
+
toolExecutor.setSandbox(acquired)
|
|
2391
|
+
|
|
2392
|
+
await eventTranslator.emitEvent({
|
|
2393
|
+
type: 'sandbox_created',
|
|
2394
|
+
turnId: ctx.turnId,
|
|
2395
|
+
sandboxId: acquired.id,
|
|
2396
|
+
environment: acquired.environment,
|
|
2397
|
+
})
|
|
2398
|
+
}
|
|
2399
|
+
const logSandboxCreated = (acquired: Sandbox): void => {
|
|
2400
|
+
ctx.log.info('Sandbox created for run', {
|
|
2401
|
+
'namzu.sandbox.id': acquired.id,
|
|
2402
|
+
'namzu.execution.environment': acquired.environment,
|
|
2403
|
+
'namzu.runtime.root_dir': acquired.rootDir,
|
|
2404
|
+
})
|
|
2405
|
+
}
|
|
2406
|
+
applyAcquisitionStop = (acquisition) => {
|
|
2362
2407
|
if (acquisition.createPending) {
|
|
2363
2408
|
ctx.log.warn(
|
|
2364
2409
|
'Sandbox creation was unsettled when the turn stopped; any returned handle will be released, but a remote allocation hidden behind a lost response requires provider-side reconciliation or a fleet reaper',
|
|
2365
2410
|
{
|
|
2366
2411
|
[NAMZU.TURN_ID]: ctx.turnId,
|
|
2367
|
-
'namzu.sandbox.provider_id':
|
|
2412
|
+
'namzu.sandbox.provider_id': sandboxProvider.id,
|
|
2368
2413
|
},
|
|
2369
2414
|
)
|
|
2370
2415
|
}
|
|
@@ -2374,32 +2419,40 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
2374
2419
|
ctx.recorder.setStopReason('timeout')
|
|
2375
2420
|
ctx.log.warn('Sandbox creation exhausted the turn timeout', {
|
|
2376
2421
|
[NAMZU.TURN_ID]: ctx.turnId,
|
|
2377
|
-
'namzu.sandbox.provider_id':
|
|
2422
|
+
'namzu.sandbox.provider_id': sandboxProvider.id,
|
|
2378
2423
|
...errorAttributes(acquisition.error),
|
|
2379
2424
|
})
|
|
2380
2425
|
}
|
|
2381
|
-
yield* resultAssembler.completeTurn(rootSpan)
|
|
2382
|
-
// The turn HAS settled, so the outer `finally` must not read
|
|
2383
|
-
// this as an abandonment — it would persist a second time.
|
|
2384
|
-
settled = true
|
|
2385
|
-
return await resultAssembler.finalize()
|
|
2386
2426
|
}
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2427
|
+
if (acquireMode === 'eager') {
|
|
2428
|
+
const acquisition = await startAcquisition(ctx.abortController.signal)
|
|
2429
|
+
if (acquisition.kind !== 'created') {
|
|
2430
|
+
applyAcquisitionStop(acquisition)
|
|
2431
|
+
yield* resultAssembler.completeTurn(rootSpan)
|
|
2432
|
+
// The turn HAS settled, so the outer `finally` must not read
|
|
2433
|
+
// this as an abandonment — it would persist a second time.
|
|
2434
|
+
settled = true
|
|
2435
|
+
return await resultAssembler.finalize()
|
|
2436
|
+
}
|
|
2437
|
+
await publishSandbox(acquisition.sandbox)
|
|
2438
|
+
yield* eventTranslator.drainPending()
|
|
2439
|
+
logSandboxCreated(acquisition.sandbox)
|
|
2440
|
+
} else {
|
|
2441
|
+
// The handle is published, and `sandbox_created` emitted, when
|
|
2442
|
+
// the provider answers. Until then the executor holds tool
|
|
2443
|
+
// admission on the gate, so nothing runs without it.
|
|
2444
|
+
const deferred = createDeferredSandbox({
|
|
2445
|
+
turnSignal: ctx.abortController.signal,
|
|
2446
|
+
acquire: startAcquisition,
|
|
2447
|
+
publish: async (acquired) => {
|
|
2448
|
+
await publishSandbox(acquired)
|
|
2449
|
+
logSandboxCreated(acquired)
|
|
2450
|
+
},
|
|
2451
|
+
})
|
|
2452
|
+
deferredSandbox = deferred
|
|
2453
|
+
toolExecutor.setSandboxGate(() => deferred.ensure())
|
|
2454
|
+
if (acquireMode === 'concurrent') deferred.start()
|
|
2455
|
+
}
|
|
2403
2456
|
}
|
|
2404
2457
|
|
|
2405
2458
|
// Before the first model call: the cheapest place to refuse, since
|
|
@@ -2525,6 +2578,14 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
2525
2578
|
|
|
2526
2579
|
yield* iterationOrchestrator.runLoop()
|
|
2527
2580
|
|
|
2581
|
+
// A turn that never needed the sandbox still settles it: a failure or
|
|
2582
|
+
// a timeout ends the turn the way an eager acquisition would have, and
|
|
2583
|
+
// a handle that arrives after that is released by the lifecycle.
|
|
2584
|
+
if (deferredSandbox) {
|
|
2585
|
+
await deferredSandbox.settle()
|
|
2586
|
+
yield* eventTranslator.drainPending()
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2528
2589
|
yield* finalizeTurn({
|
|
2529
2590
|
ctx,
|
|
2530
2591
|
params,
|
|
@@ -2536,6 +2597,16 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
2536
2597
|
rootSpan,
|
|
2537
2598
|
})
|
|
2538
2599
|
} catch (err) {
|
|
2600
|
+
// A deferred acquisition stopped (cancelled, or out of time) and the
|
|
2601
|
+
// turn found out at its first tool call or at its end. Settled the
|
|
2602
|
+
// way the eager path settles it, with nothing else to report.
|
|
2603
|
+
if (err instanceof SandboxAcquisitionStopped) {
|
|
2604
|
+
applyAcquisitionStop(err.acquisition)
|
|
2605
|
+
yield* eventTranslator.drainPending()
|
|
2606
|
+
yield* resultAssembler.completeTurn(rootSpan)
|
|
2607
|
+
settled = true
|
|
2608
|
+
return await resultAssembler.finalize()
|
|
2609
|
+
}
|
|
2539
2610
|
// Another turn is active in this session: nothing of this turn was
|
|
2540
2611
|
// written, so there is nothing to settle. The caller gets the
|
|
2541
2612
|
// refusal itself.
|
|
@@ -2546,6 +2617,10 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
2546
2617
|
yield* eventTranslator.drainPending()
|
|
2547
2618
|
yield* resultAssembler.handleError(err, rootSpan)
|
|
2548
2619
|
} finally {
|
|
2620
|
+
// A pending acquisition is withdrawn and awaited first, so a handle
|
|
2621
|
+
// that arrives now is either published (and released below) or
|
|
2622
|
+
// handed to the late-teardown path. Nothing is left allocated.
|
|
2623
|
+
await deferredSandbox?.cancel()
|
|
2549
2624
|
yield* releaseTurnResources({
|
|
2550
2625
|
ctx,
|
|
2551
2626
|
eventTranslator,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
} from '../../../constants/tools/index.js'
|
|
16
16
|
import { withStreamedMessageIdentity } from '../../../manager/session/streamed-message-identity.js'
|
|
17
17
|
import { renderSkillsSection } from '../../../persona/assembler.js'
|
|
18
|
-
import { resolveProviderCapabilities } from '../../../provider/capabilities.js'
|
|
18
|
+
import { modelReadsImages, resolveProviderCapabilities } from '../../../provider/capabilities.js'
|
|
19
19
|
import { collectChatCompletion } from '../../../provider/collect-chat-completion.js'
|
|
20
20
|
import { renderToolSchema } from '../../../registry/tool/schema.js'
|
|
21
21
|
import { formatCompletionNotification } from '../../../scheduler/completion-inbox.js'
|
|
@@ -54,6 +54,7 @@ import { stableDigest } from '../../../utils/hash.js'
|
|
|
54
54
|
import { generateMessageId } from '../../../utils/id.js'
|
|
55
55
|
import { parseStructuredResultJson } from '../../../utils/structured-result-json.js'
|
|
56
56
|
import { createCallbackInference } from '../callback-inference.js'
|
|
57
|
+
import { SandboxAcquisitionStopped } from '../deferred-sandbox.js'
|
|
57
58
|
import { deliverDurableInbound } from '../durable-inbound.js'
|
|
58
59
|
import type { ToolCallOutcome } from '../executor.js'
|
|
59
60
|
import { projectObservationContext } from '../observation-context.js'
|
|
@@ -80,17 +81,27 @@ import { runAdvisoryPhase } from './phases/advisory.js'
|
|
|
80
81
|
import { runIterationCheckpoint } from './phases/checkpoint.js'
|
|
81
82
|
import { compactionBreaker } from './phases/compaction-breaker.js'
|
|
82
83
|
import {
|
|
84
|
+
AUTOMATIC_COMPACTION_FAILED,
|
|
85
|
+
OVERFLOW_RELIEF_LIMIT,
|
|
83
86
|
activeContextWindow,
|
|
87
|
+
clearCompactionErrorNote,
|
|
88
|
+
compactionErrorNote,
|
|
84
89
|
measureContext,
|
|
85
90
|
relieveOverflow,
|
|
86
91
|
runCompactionCheck,
|
|
92
|
+
stopOnRefill,
|
|
87
93
|
} from './phases/compaction.js'
|
|
88
94
|
import { runHandoffPause } from './phases/handoff.js'
|
|
89
95
|
import type { IterationContext } from './phases/index.js'
|
|
90
96
|
import { runPlanGate } from './phases/plan.js'
|
|
91
97
|
import { runToolReview } from './phases/tool-review.js'
|
|
92
98
|
import { refreshWorkingMemory, splitWorkingMemoryForRequest } from './phases/working-memory.js'
|
|
93
|
-
import {
|
|
99
|
+
import {
|
|
100
|
+
type TextOnlyModelRecovery,
|
|
101
|
+
type TextOnlyModelRefusal,
|
|
102
|
+
streamWithProviderRejectedImageRecovery,
|
|
103
|
+
} from './provider-rejected-image.js'
|
|
104
|
+
import { type MediaRemoval, RequestRepair } from './request-repair.js'
|
|
94
105
|
import {
|
|
95
106
|
type StepShaping,
|
|
96
107
|
appendWorkContext,
|
|
@@ -141,6 +152,11 @@ const CLOSING_RESPONSE_GUIDANCE =
|
|
|
141
152
|
|
|
142
153
|
export { awaitedJobGraceMs, settleGraceMs } from './outstanding-work.js'
|
|
143
154
|
|
|
155
|
+
/** One provider's model, as {@link IterationOrchestrator} remembers it. */
|
|
156
|
+
function textOnlyKey(providerId: string, model: string): string {
|
|
157
|
+
return `${providerId}\u0000${model}`
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
export class IterationOrchestrator {
|
|
145
161
|
private ctx: IterationContext
|
|
146
162
|
private advisoryTurn:
|
|
@@ -188,6 +204,73 @@ export class IterationOrchestrator {
|
|
|
188
204
|
}
|
|
189
205
|
/** Rich tool blocks already reported; durable history is scanned every turn. */
|
|
190
206
|
private readonly warnedRichToolResults = new Set<string>()
|
|
207
|
+
/** What this run has learned about requests its provider rejects as invalid, and how to repair them. */
|
|
208
|
+
private readonly requestRepair = new RequestRepair({
|
|
209
|
+
debug: (message) => this.ctx.log.debug(message),
|
|
210
|
+
onMediaRemoved: (removal) => this.noteMediaRemoved(removal),
|
|
211
|
+
servedBy: () => this.ctx.servingMember?.(),
|
|
212
|
+
})
|
|
213
|
+
/**
|
|
214
|
+
* Provider and model pairs that refused a request for holding an image; they
|
|
215
|
+
* get none for the rest of the run. Keyed by both: one model id can be served
|
|
216
|
+
* by two members of a fallback chain, and only the one that refused is known.
|
|
217
|
+
*/
|
|
218
|
+
private readonly textOnlyModels = new Set<string>()
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The member a request for `model` goes to first. A fallback chain keeps
|
|
222
|
+
* serving from the member it last moved to, so that member, not the head,
|
|
223
|
+
* is the one whose reading of images counts.
|
|
224
|
+
*/
|
|
225
|
+
private servingModel(model: string): { readonly providerId: string; readonly model: string } {
|
|
226
|
+
const serving = this.ctx.servingMember?.()
|
|
227
|
+
return {
|
|
228
|
+
providerId: serving?.providerId ?? this.ctx.provider.id,
|
|
229
|
+
model: serving?.model ?? model,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Whether a request for `model` may carry images: the driver's catalogue and this run's own evidence agree. */
|
|
234
|
+
private modelReadsImages(model: string): boolean {
|
|
235
|
+
const serving = this.servingModel(model)
|
|
236
|
+
return (
|
|
237
|
+
!this.textOnlyModels.has(textOnlyKey(serving.providerId, serving.model)) &&
|
|
238
|
+
modelReadsImages(this.ctx.provider, model)
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** What a request for `model` does when its model turns out to read text only. */
|
|
243
|
+
private textOnlyRecovery(model: string): TextOnlyModelRecovery {
|
|
244
|
+
return {
|
|
245
|
+
servedBy: () => this.servingModel(model),
|
|
246
|
+
onAccepted: (refused) => this.noteTextOnlyModel(refused),
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* A provider refused a request for holding an image its model cannot read, and
|
|
252
|
+
* the retry without images was accepted. Stop sending images to it, and say so
|
|
253
|
+
* once, so the host can remember the model beyond this run.
|
|
254
|
+
*/
|
|
255
|
+
private async noteTextOnlyModel(refused: TextOnlyModelRefusal): Promise<void> {
|
|
256
|
+
const key = textOnlyKey(refused.providerId, refused.model ?? '')
|
|
257
|
+
if (this.textOnlyModels.has(key)) return
|
|
258
|
+
this.textOnlyModels.add(key)
|
|
259
|
+
const { providerId, model } = refused
|
|
260
|
+
this.ctx.log.warn('Capability mismatch: the model cannot read images', {
|
|
261
|
+
[GENAI.SYSTEM]: providerId,
|
|
262
|
+
[GENAI.REQUEST_MODEL]: model ?? '',
|
|
263
|
+
})
|
|
264
|
+
await this.ctx.emitEvent?.({
|
|
265
|
+
type: 'capability_warning',
|
|
266
|
+
turnId: this.ctx.recorder.turnId,
|
|
267
|
+
capability: 'vision',
|
|
268
|
+
contentSource: 'history',
|
|
269
|
+
providerId,
|
|
270
|
+
...(model !== undefined ? { model } : {}),
|
|
271
|
+
message: `Model '${model ?? 'unknown'}' on provider '${providerId}' refused a request that held an image, so images are left out of its requests. They stay in the conversation and are sent again to a model that reads them.`,
|
|
272
|
+
})
|
|
273
|
+
}
|
|
191
274
|
/**
|
|
192
275
|
* The previous iteration held a `stopWhen` decision open for a worker.
|
|
193
276
|
*
|
|
@@ -442,16 +525,19 @@ export class IterationOrchestrator {
|
|
|
442
525
|
// checkpoint names the message the turn is answering.
|
|
443
526
|
await this.ctx.checkpointMgr.markLoopStart?.(recorder)
|
|
444
527
|
|
|
445
|
-
//
|
|
528
|
+
// Context-overflow relief is a two-rung ladder per *stuck point*, not per
|
|
529
|
+
// turn, as in the reference terminal (at most two reactive compactions per
|
|
530
|
+
// overflow): the first relief, then one deeper pass if the retry still
|
|
531
|
+
// overflows.
|
|
446
532
|
//
|
|
447
|
-
// The
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
let
|
|
533
|
+
// The limit exists so that an overflow that survives both rungs — an
|
|
534
|
+
// irreducible prompt — stops instead of looping. It was never meant to
|
|
535
|
+
// disarm the mechanism for the rest of the turn, which is what a
|
|
536
|
+
// turn-scoped flag did: one relief at iteration 3 left iteration 40 to die
|
|
537
|
+
// on an overflow with obvious moves left. The count is cleared by a turn
|
|
538
|
+
// that actually succeeded, which is the evidence that the turn is no
|
|
539
|
+
// longer stuck.
|
|
540
|
+
let overflowReliefs = 0
|
|
455
541
|
// The output cap a provider told us fits beside this prompt. One request's
|
|
456
542
|
// worth: it is cleared by a call that went through.
|
|
457
543
|
let outputCapOverride: number | undefined
|
|
@@ -814,6 +900,7 @@ export class IterationOrchestrator {
|
|
|
814
900
|
this.projectObservations(requestHistory),
|
|
815
901
|
this.ctx.turnConfig.maxRequestRichContentBytes ??
|
|
816
902
|
DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
903
|
+
{ imagesReadable: this.modelReadsImages(stepModel) },
|
|
817
904
|
)
|
|
818
905
|
appendWorkContext(shaping, messages, iterationNum, step)
|
|
819
906
|
if (closingDirective) messages.push(closingDirective)
|
|
@@ -989,6 +1076,8 @@ export class IterationOrchestrator {
|
|
|
989
1076
|
stepMessageId,
|
|
990
1077
|
{
|
|
991
1078
|
onAccepted: (identity) => this.acceptProviderRejectedImage(identity),
|
|
1079
|
+
textOnly: this.textOnlyRecovery(stepModel),
|
|
1080
|
+
repair: this.requestRepair,
|
|
992
1081
|
},
|
|
993
1082
|
Boolean(
|
|
994
1083
|
this.ctx.reviewAnswer || this.ctx.structuredOutput?.review || this.ctx.advisoryCtx,
|
|
@@ -1042,7 +1131,8 @@ export class IterationOrchestrator {
|
|
|
1042
1131
|
|
|
1043
1132
|
// The turn went through, so the turn is not sitting on an
|
|
1044
1133
|
// irreducible prompt any more. Re-arm relief for the next one.
|
|
1045
|
-
|
|
1134
|
+
overflowReliefs = 0
|
|
1135
|
+
clearCompactionErrorNote(this.ctx)
|
|
1046
1136
|
outputCapOverride = undefined
|
|
1047
1137
|
if (response.finishReason !== 'length') this.outputContinuations = 0
|
|
1048
1138
|
|
|
@@ -1895,6 +1985,12 @@ export class IterationOrchestrator {
|
|
|
1895
1985
|
yield* this.ctx.drainPending()
|
|
1896
1986
|
} catch (err) {
|
|
1897
1987
|
const cancelled = this.ctx.abortController.signal.aborted
|
|
1988
|
+
// A deferred sandbox that was cancelled or ran out of time. The
|
|
1989
|
+
// turn settles it as `cancelled` / `timeout` in `query()`, which is
|
|
1990
|
+
// not a failure of this iteration: it is recorded the way a turn
|
|
1991
|
+
// stopped by its own abort or timeout is (a cancelled step, no
|
|
1992
|
+
// error span), so the ledger and the stop reason agree.
|
|
1993
|
+
const sandboxStopped = err instanceof SandboxAcquisitionStopped
|
|
1898
1994
|
|
|
1899
1995
|
// This iteration gets a step too, and it is the one the
|
|
1900
1996
|
// argument three hundred lines above was actually about.
|
|
@@ -1955,12 +2051,13 @@ export class IterationOrchestrator {
|
|
|
1955
2051
|
startedAt: stepStartedAt,
|
|
1956
2052
|
usageBefore,
|
|
1957
2053
|
costBefore,
|
|
1958
|
-
unfinished:
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
2054
|
+
unfinished:
|
|
2055
|
+
cancelled || sandboxStopped
|
|
2056
|
+
? { finishReason: 'cancelled' }
|
|
2057
|
+
: {
|
|
2058
|
+
finishReason: 'error',
|
|
2059
|
+
failure: describeStepFailure(err, this.ctx.provider.id),
|
|
2060
|
+
},
|
|
1964
2061
|
})
|
|
1965
2062
|
}
|
|
1966
2063
|
|
|
@@ -2020,22 +2117,58 @@ export class IterationOrchestrator {
|
|
|
2020
2117
|
}
|
|
2021
2118
|
|
|
2022
2119
|
if (
|
|
2023
|
-
!overflowRelieved &&
|
|
2024
2120
|
!(err instanceof AnswerReviewFailure) &&
|
|
2025
2121
|
classifyProviderError(err, this.ctx.provider.id).code === 'context_length_exceeded'
|
|
2026
2122
|
) {
|
|
2027
|
-
|
|
2028
|
-
|
|
2123
|
+
// The rapid-refill check comes first, before the first reactive relief
|
|
2124
|
+
// of an overflow, as in the reference terminal. A relief that follows
|
|
2125
|
+
// the previous compaction within the refill window counts toward the
|
|
2126
|
+
// limit, so a tool that re-fills the window every time it is called
|
|
2127
|
+
// stops the turn instead of being cleared and called again without end.
|
|
2128
|
+
// The deeper rung of the same overflow is not counted again.
|
|
2129
|
+
if (overflowReliefs === 0 && (await stopOnRefill(this.ctx))) {
|
|
2130
|
+
if (iterationActivity) {
|
|
2131
|
+
this.ctx.activityStore.fail(iterationActivity.id, toErrorMessage(err))
|
|
2132
|
+
}
|
|
2133
|
+
if (this.stopForCompactionBreaker()) break
|
|
2134
|
+
}
|
|
2135
|
+
// A rung that sheds too little to change the verdict is not worth a
|
|
2136
|
+
// provider call; climb to the next one at once.
|
|
2137
|
+
let shed = false
|
|
2138
|
+
while (!shed && overflowReliefs < OVERFLOW_RELIEF_LIMIT) {
|
|
2139
|
+
overflowReliefs++
|
|
2140
|
+
shed = await relieveOverflow(this.ctx, { deeper: overflowReliefs > 1 })
|
|
2141
|
+
}
|
|
2029
2142
|
if (shed) {
|
|
2143
|
+
// Clearing results is a compaction too: only summaries and reducers
|
|
2144
|
+
// recorded themselves, so a clear-only relief was invisible to the
|
|
2145
|
+
// breaker.
|
|
2146
|
+
compactionBreaker(this.ctx)?.recordCompaction(iterationNum)
|
|
2030
2147
|
this.ctx.log.info('Retrying the turn after relieving a context overflow', {
|
|
2031
2148
|
[NAMZU.TURN_ID]: recorder.turnId,
|
|
2032
2149
|
[NAMZU.ITERATION]: iterationNum,
|
|
2150
|
+
'namzu.runtime.relief_rung': overflowReliefs,
|
|
2033
2151
|
})
|
|
2034
2152
|
if (iterationActivity) {
|
|
2035
2153
|
this.ctx.activityStore.complete(iterationActivity.id)
|
|
2036
2154
|
}
|
|
2037
2155
|
continue
|
|
2038
2156
|
}
|
|
2157
|
+
// Out of rungs. The conversation is kept; say why compaction could not
|
|
2158
|
+
// make room when it errored, as the reference does.
|
|
2159
|
+
const cause = compactionErrorNote(this.ctx)
|
|
2160
|
+
if (cause !== undefined && err instanceof Error) {
|
|
2161
|
+
try {
|
|
2162
|
+
err.message = `${err.message}${AUTOMATIC_COMPACTION_FAILED}${cause}`
|
|
2163
|
+
} catch {
|
|
2164
|
+
// A frozen error keeps its own message; the cause stays in the log.
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
if (sandboxStopped) {
|
|
2170
|
+
if (iterationActivity) this.ctx.activityStore.complete(iterationActivity.id)
|
|
2171
|
+
throw err
|
|
2039
2172
|
}
|
|
2040
2173
|
|
|
2041
2174
|
if (iterationActivity) {
|
|
@@ -2643,6 +2776,32 @@ export class IterationOrchestrator {
|
|
|
2643
2776
|
.some((t) => t.state !== 'completed' && t.state !== 'failed' && t.state !== 'canceled')
|
|
2644
2777
|
}
|
|
2645
2778
|
|
|
2779
|
+
/** Media the provider could not process was left out of a request that then went through. */
|
|
2780
|
+
private async noteMediaRemoved(removal: MediaRemoval): Promise<void> {
|
|
2781
|
+
this.ctx.log.warn('Media the provider rejected was left out of the request', {
|
|
2782
|
+
[GENAI.SYSTEM]: this.ctx.provider.id,
|
|
2783
|
+
'namzu.request.media_removed': removal.count,
|
|
2784
|
+
})
|
|
2785
|
+
const noun = removal.count === 1 ? 'a file' : `${removal.count} files`
|
|
2786
|
+
const it = removal.count === 1 ? 'it' : 'they'
|
|
2787
|
+
await this.ctx.emitEvent?.({
|
|
2788
|
+
type: 'capability_warning',
|
|
2789
|
+
turnId: this.ctx.recorder.turnId,
|
|
2790
|
+
capability: removal.kind === 'document' ? 'documents' : 'vision',
|
|
2791
|
+
// The person's own latest attachment is named as such; a count limit is
|
|
2792
|
+
// not a fault in any file.
|
|
2793
|
+
contentSource: removal.current
|
|
2794
|
+
? 'media-removed-current'
|
|
2795
|
+
: removal.limit
|
|
2796
|
+
? 'media-limit'
|
|
2797
|
+
: 'media-removed',
|
|
2798
|
+
providerId: this.ctx.provider.id,
|
|
2799
|
+
message: removal.limit
|
|
2800
|
+
? `The conversation holds more files than the provider accepts at once, so ${noun} (the oldest) ${removal.count === 1 ? 'was' : 'were'} left out of the request. The conversation still holds ${it}.`
|
|
2801
|
+
: `The provider could not process ${noun}${removal.current ? ' attached to your latest message' : ' in this conversation'}, so ${it} ${removal.count === 1 ? 'was' : 'were'} left out of the request. The conversation still holds ${it}.`,
|
|
2802
|
+
})
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2646
2805
|
private async acceptProviderRejectedImage(identity: RequestImageIdentity): Promise<void> {
|
|
2647
2806
|
const repaired = markProviderRejectedImage(this.ctx.recorder.messages, identity)
|
|
2648
2807
|
if (repaired.count === 0) {
|
|
@@ -2700,6 +2859,7 @@ export class IterationOrchestrator {
|
|
|
2700
2859
|
const finalMessages = projectRequestRichContent(
|
|
2701
2860
|
this.projectObservations(finalHistory),
|
|
2702
2861
|
this.ctx.turnConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
2862
|
+
{ imagesReadable: this.modelReadsImages(model) },
|
|
2703
2863
|
)
|
|
2704
2864
|
appendWorkContext(this.stepShaping(), finalMessages, this.steps.length + 1, { model })
|
|
2705
2865
|
// The closing directive is the last thing the model reads: after
|
|
@@ -2746,8 +2906,13 @@ export class IterationOrchestrator {
|
|
|
2746
2906
|
signal: this.ctx.abortController.signal,
|
|
2747
2907
|
} satisfies import('../../../types/provider/index.js').ChatCompletionParams
|
|
2748
2908
|
const response = await collectChatCompletion(
|
|
2749
|
-
streamWithProviderRejectedImageRecovery(
|
|
2750
|
-
this.
|
|
2909
|
+
streamWithProviderRejectedImageRecovery(
|
|
2910
|
+
this.ctx.provider,
|
|
2911
|
+
finalParams,
|
|
2912
|
+
(identity) => this.acceptProviderRejectedImage(identity),
|
|
2913
|
+
undefined,
|
|
2914
|
+
this.textOnlyRecovery(model),
|
|
2915
|
+
this.requestRepair,
|
|
2751
2916
|
),
|
|
2752
2917
|
)
|
|
2753
2918
|
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - Three failures in a row (a pass that ran and shed nothing): every one of
|
|
5
5
|
* them paid for a summary or a reducer call and left the history as it was,
|
|
6
|
-
* so a fourth would do the same.
|
|
7
|
-
* turn carries on
|
|
8
|
-
*
|
|
6
|
+
* so a fourth would do the same. Proactive compaction is skipped from then on
|
|
7
|
+
* and the turn carries on, silently as far as the person is concerned (a log
|
|
8
|
+
* line and a `breaker_open` event, no sentence); a pass the provider forces
|
|
9
|
+
* by rejecting the prompt is not gated, so the reactive ladder still runs.
|
|
9
10
|
* - Three compactions in a row, each followed by the context filling back up
|
|
10
11
|
* within three iterations: something in the loop (a huge file read, a tool
|
|
11
12
|
* that prints without limit) refills the window faster than any summary can
|
|
@@ -27,12 +28,13 @@ export const COMPACTION_REFILL_ITERATIONS = 3
|
|
|
27
28
|
/** Consecutive refills that stop the turn. */
|
|
28
29
|
export const COMPACTION_REFILL_LIMIT = 3
|
|
29
30
|
|
|
30
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* One short line, shown once, when the turn stops on refills. It names the
|
|
33
|
+
* cause and a step that keeps the conversation: the reference terminal also
|
|
34
|
+
* offers to start over here, and Namzu never does.
|
|
35
|
+
*/
|
|
31
36
|
export const COMPACTION_REFILL_MESSAGE =
|
|
32
|
-
'The context filled up again right after being compacted, three times in a row. A file or tool output is probably too
|
|
33
|
-
/** One short line, shown once, when compaction stops after failures. */
|
|
34
|
-
export const COMPACTION_FAILURE_MESSAGE =
|
|
35
|
-
'Automatic compaction stopped after three failed attempts. Run /compact yourself, or start fresh with /clear.'
|
|
37
|
+
'The context filled up again right after being compacted, three times in a row. A file being read or a tool output is probably too large for the context window. Try reading it in smaller parts.'
|
|
36
38
|
|
|
37
39
|
export class CompactionBreaker {
|
|
38
40
|
private failures = 0
|