@namzu/sdk 60.0.0 → 60.2.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 +40 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +1 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +19 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/acp/update.d.ts.map +1 -1
- package/dist/bridge/acp/update.js +1 -0
- package/dist/bridge/acp/update.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +28 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/constants/continuation.d.ts +9 -0
- package/dist/constants/continuation.d.ts.map +1 -1
- package/dist/constants/continuation.js +9 -0
- package/dist/constants/continuation.js.map +1 -1
- package/dist/contracts/session/api.d.ts +8 -2
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +7 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +20 -0
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/peers/dir.d.ts.map +1 -1
- package/dist/peers/dir.js +3 -0
- package/dist/peers/dir.js.map +1 -1
- package/dist/provider/collect-chat-completion.d.ts.map +1 -1
- package/dist/provider/collect-chat-completion.js +4 -0
- package/dist/provider/collect-chat-completion.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +11 -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/mock.d.ts +2 -0
- package/dist/provider/mock.d.ts.map +1 -1
- package/dist/provider/mock.js +16 -8
- package/dist/provider/mock.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts.map +1 -1
- package/dist/provider/model-window-suffix.js +1 -0
- package/dist/provider/model-window-suffix.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- 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 +1 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +17 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +23 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +88 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +14 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +14 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/effort-clamp.d.ts +13 -0
- package/dist/runtime/query/iteration/effort-clamp.d.ts.map +1 -0
- package/dist/runtime/query/iteration/effort-clamp.js +42 -0
- package/dist/runtime/query/iteration/effort-clamp.js.map +1 -0
- package/dist/runtime/query/iteration/index.d.ts +34 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +199 -6
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +2 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +6 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +6 -0
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +12 -2
- package/dist/runtime/query/review-policy.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 +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +6 -5
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/session/paths.d.ts +15 -22
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +19 -46
- package/dist/session/paths.js.map +1 -1
- package/dist/store/memory/disk.d.ts.map +1 -1
- package/dist/store/memory/disk.js +1 -1
- package/dist/store/memory/disk.js.map +1 -1
- package/dist/store/memory/markdown.d.ts.map +1 -1
- package/dist/store/memory/markdown.js +1 -1
- package/dist/store/memory/markdown.js.map +1 -1
- package/dist/store/memory/operation-lock.d.ts +20 -4
- package/dist/store/memory/operation-lock.d.ts.map +1 -1
- package/dist/store/memory/operation-lock.js +235 -58
- package/dist/store/memory/operation-lock.js.map +1 -1
- package/dist/store/session-log/core.d.ts +43 -0
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +190 -3
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/disk.d.ts +1 -1
- package/dist/store/session-log/disk.d.ts.map +1 -1
- package/dist/store/session-log/disk.js +1 -0
- package/dist/store/session-log/disk.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -1
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -1
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -1
- package/dist/tools/builtins/bash-cwd.js +2 -3
- package/dist/tools/builtins/bash-cwd.js.map +1 -1
- package/dist/tools/builtins/read-media.js +3 -3
- package/dist/tools/builtins/read-media.js.map +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +13 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/agent/query.d.ts +2 -0
- package/dist/types/agent/query.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +8 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +1 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +19 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +7 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/index.d.ts +1 -1
- package/dist/types/provider/index.d.ts.map +1 -1
- package/dist/types/provider/interface.d.ts +15 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +72 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js +4 -0
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +34 -34
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +5 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/stop-reason.d.ts +11 -0
- package/dist/types/session/stop-reason.d.ts.map +1 -1
- package/dist/utils/temp.d.ts +82 -0
- package/dist/utils/temp.d.ts.map +1 -0
- package/dist/utils/temp.js +196 -0
- package/dist/utils/temp.js.map +1 -0
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +1 -0
- package/src/bridge/a2a/mapper.ts +22 -0
- package/src/bridge/acp/update.ts +6 -1
- package/src/bridge/sse/mapper.ts +29 -0
- package/src/constants/continuation.ts +13 -0
- package/src/contracts/session/api.ts +8 -0
- package/src/manager/session/turn-recorder.ts +22 -0
- package/src/peers/dir.ts +3 -0
- package/src/provider/collect-chat-completion.ts +3 -0
- package/src/provider/fallback.ts +17 -0
- package/src/provider/idle-timeout.ts +9 -0
- package/src/provider/mock.ts +19 -9
- package/src/provider/model-window-suffix.ts +1 -0
- package/src/provider/retry.ts +9 -0
- package/src/public-runtime.ts +13 -0
- package/src/runtime/query/events.ts +19 -0
- package/src/runtime/query/executor.ts +92 -8
- package/src/runtime/query/index.ts +27 -0
- package/src/runtime/query/iteration/effort-clamp.ts +46 -0
- package/src/runtime/query/iteration/index.ts +260 -6
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +6 -0
- package/src/runtime/query/iteration/stream-turn.ts +5 -0
- package/src/runtime/query/review-policy.ts +13 -2
- package/src/runtime/query/tooling.ts +3 -0
- package/src/sandbox/provider/local.ts +8 -5
- package/src/session/paths.ts +32 -57
- package/src/store/memory/disk.ts +1 -0
- package/src/store/memory/markdown.ts +1 -0
- package/src/store/memory/operation-lock.ts +288 -57
- package/src/store/session-log/core.ts +235 -3
- package/src/store/session-log/disk.ts +7 -1
- package/src/store/session-log/index.ts +4 -0
- package/src/tools/builtins/bash-cwd.ts +2 -3
- package/src/tools/builtins/read-media.ts +3 -3
- package/src/turn/reporter.ts +14 -0
- package/src/types/agent/query.ts +2 -0
- package/src/types/hitl/index.ts +8 -0
- package/src/types/message/index.ts +5 -3
- package/src/types/provider/chat.ts +20 -0
- package/src/types/provider/config.ts +7 -0
- package/src/types/provider/index.ts +1 -0
- package/src/types/provider/interface.ts +20 -0
- package/src/types/provider/stream.ts +6 -0
- package/src/types/session/events.ts +106 -6
- package/src/types/session/records.ts +5 -0
- package/src/types/session/stop-reason.ts +11 -0
- package/src/utils/temp.ts +250 -0
|
@@ -211,8 +211,59 @@ export interface SessionLog {
|
|
|
211
211
|
/** The folded conversation (spec §4.5), spills read back. */
|
|
212
212
|
messages(options?: { readonly throughSeq?: number }): Promise<Message[]>
|
|
213
213
|
readSpill(ref: SpillRef, options?: SpillReadOptions): Promise<string>
|
|
214
|
+
/**
|
|
215
|
+
* Be told when the log stops being able to write (the disk is full) and
|
|
216
|
+
* when it can write again. Returns the function that stops the
|
|
217
|
+
* notifications. A log that cannot degrade does not offer this.
|
|
218
|
+
*/
|
|
219
|
+
onWriteHealth?(listener: (change: SessionLogWriteHealth) => void): () => void
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Why a log's writes are failing; each is a full or unwritable volume. */
|
|
223
|
+
export type SessionLogDegradedCause = 'disk_full' | 'quota_exceeded' | 'read_only'
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A change in whether a log can write. While `degraded` the session goes on
|
|
227
|
+
* in memory and the records not yet written wait in a bounded buffer.
|
|
228
|
+
*/
|
|
229
|
+
export type SessionLogWriteHealth =
|
|
230
|
+
| {
|
|
231
|
+
readonly state: 'degraded'
|
|
232
|
+
readonly cause: SessionLogDegradedCause
|
|
233
|
+
/** Records waiting to be written, including the one that failed. */
|
|
234
|
+
readonly bufferedRecords: number
|
|
235
|
+
readonly bufferedBytes: number
|
|
236
|
+
/**
|
|
237
|
+
* Set on the one notification sent when the buffer first overflowed
|
|
238
|
+
* and the oldest waiting records were discarded.
|
|
239
|
+
*/
|
|
240
|
+
readonly dropping?: true
|
|
241
|
+
}
|
|
242
|
+
| {
|
|
243
|
+
readonly state: 'recovered'
|
|
244
|
+
/** Records written from the buffer, in order. */
|
|
245
|
+
readonly flushedRecords: number
|
|
246
|
+
/** Records discarded while the buffer was full; the log has a gap where they were. */
|
|
247
|
+
readonly droppedRecords: number
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The cause when `error` is the volume refusing writes, else `undefined`. */
|
|
251
|
+
export function sessionLogDegradedCause(error: unknown): SessionLogDegradedCause | undefined {
|
|
252
|
+
switch ((error as NodeJS.ErrnoException | undefined)?.code) {
|
|
253
|
+
case 'ENOSPC':
|
|
254
|
+
return 'disk_full'
|
|
255
|
+
case 'EDQUOT':
|
|
256
|
+
return 'quota_exceeded'
|
|
257
|
+
case 'EROFS':
|
|
258
|
+
return 'read_only'
|
|
259
|
+
default:
|
|
260
|
+
return undefined
|
|
261
|
+
}
|
|
214
262
|
}
|
|
215
263
|
|
|
264
|
+
/** Default bound on records held in memory while a log cannot write. */
|
|
265
|
+
export const DEFAULT_DEGRADED_BUFFER_BYTES = 8 * 1024 * 1024
|
|
266
|
+
|
|
216
267
|
/** A draft that is not a valid session record once enveloped. */
|
|
217
268
|
export class InvalidSessionRecordError extends Error {
|
|
218
269
|
override readonly name = 'InvalidSessionRecordError'
|
|
@@ -271,6 +322,12 @@ export interface SessionLogCoreOptions {
|
|
|
271
322
|
* and decisions. `all` or `none` as named.
|
|
272
323
|
*/
|
|
273
324
|
readonly sync?: 'boundaries' | 'all' | 'none'
|
|
325
|
+
/**
|
|
326
|
+
* Bytes of records kept in memory while the volume refuses writes (full,
|
|
327
|
+
* over quota, read-only). Past it the oldest are discarded. Default
|
|
328
|
+
* {@link DEFAULT_DEGRADED_BUFFER_BYTES}.
|
|
329
|
+
*/
|
|
330
|
+
readonly degradedBufferBytes?: number
|
|
274
331
|
}
|
|
275
332
|
|
|
276
333
|
const BOUNDARY_TYPES: ReadonlySet<string> = new Set([
|
|
@@ -289,6 +346,8 @@ const BOUNDARY_TYPES: ReadonlySet<string> = new Set([
|
|
|
289
346
|
|
|
290
347
|
/** Characters of a spilled message kept inline as its preview. */
|
|
291
348
|
const PREVIEW_CHARS = 2000
|
|
349
|
+
/** Added to a preview whose full body could not be written. */
|
|
350
|
+
const NOT_SAVED_NOTE = '[… the rest of this output could not be saved: the disk is full]'
|
|
292
351
|
/** Default byte bound above which a `compaction` record's summary is spilled. */
|
|
293
352
|
const COMPACTION_SPILL_ABOVE_BYTES = 256 * 1024
|
|
294
353
|
|
|
@@ -339,6 +398,17 @@ export class SessionLogCore implements SessionLog {
|
|
|
339
398
|
/** Bytes of the log this instance has verified and applied. */
|
|
340
399
|
#synced = 0
|
|
341
400
|
#torn = 0
|
|
401
|
+
readonly #degradedBufferBytes: number
|
|
402
|
+
/** Records accepted into the chain that the medium has not taken yet, oldest first. */
|
|
403
|
+
#pending: { readonly bytes: Buffer; readonly sync: boolean }[] = []
|
|
404
|
+
#pendingBytes = 0
|
|
405
|
+
/** Bytes the medium holds while records are pending. */
|
|
406
|
+
#diskBytes = 0
|
|
407
|
+
#degradedCause: SessionLogDegradedCause | undefined
|
|
408
|
+
#droppedRecords = 0
|
|
409
|
+
#droppedNoticeSent = false
|
|
410
|
+
#flushedWhileDegraded = 0
|
|
411
|
+
readonly #healthListeners = new Set<(change: SessionLogWriteHealth) => void>()
|
|
342
412
|
|
|
343
413
|
constructor(options: SessionLogCoreOptions) {
|
|
344
414
|
this.sessionId = options.sessionId
|
|
@@ -362,12 +432,40 @@ export class SessionLogCore implements SessionLog {
|
|
|
362
432
|
this.#spillAbove,
|
|
363
433
|
)
|
|
364
434
|
this.#sync = options.sync ?? 'boundaries'
|
|
435
|
+
this.#degradedBufferBytes = Math.max(
|
|
436
|
+
0,
|
|
437
|
+
options.degradedBufferBytes ?? DEFAULT_DEGRADED_BUFFER_BYTES,
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
onWriteHealth(listener: (change: SessionLogWriteHealth) => void): () => void {
|
|
442
|
+
this.#healthListeners.add(listener)
|
|
443
|
+
return () => {
|
|
444
|
+
this.#healthListeners.delete(listener)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#notify(change: SessionLogWriteHealth): void {
|
|
449
|
+
for (const listener of this.#healthListeners) {
|
|
450
|
+
try {
|
|
451
|
+
listener(change)
|
|
452
|
+
} catch {
|
|
453
|
+
// A listener that throws must not turn a survived write into a failed one.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** True while records wait in memory because the volume refuses writes. */
|
|
459
|
+
get degraded(): boolean {
|
|
460
|
+
return this.#degradedCause !== undefined
|
|
365
461
|
}
|
|
366
462
|
|
|
367
463
|
// ── state ──
|
|
368
464
|
|
|
369
465
|
/** Catch up with the medium: verify and apply whatever was appended since the last sync. */
|
|
370
466
|
async #catchUp(): Promise<void> {
|
|
467
|
+
// Records waiting for the disk are ahead of it; the lease keeps others out.
|
|
468
|
+
if (this.#pending.length > 0) return
|
|
371
469
|
const size = await this.#medium.size()
|
|
372
470
|
if (size === this.#synced + this.#torn && size >= this.#synced) return
|
|
373
471
|
if (size < this.#synced) this.#reset()
|
|
@@ -395,6 +493,8 @@ export class SessionLogCore implements SessionLog {
|
|
|
395
493
|
this.#turns = new SessionTurnState()
|
|
396
494
|
this.#synced = 0
|
|
397
495
|
this.#torn = 0
|
|
496
|
+
this.#pending = []
|
|
497
|
+
this.#pendingBytes = 0
|
|
398
498
|
}
|
|
399
499
|
|
|
400
500
|
async #requireCurrent(lease: SessionLease): Promise<void> {
|
|
@@ -461,6 +561,8 @@ export class SessionLogCore implements SessionLog {
|
|
|
461
561
|
*/
|
|
462
562
|
release(lease: SessionLease): Promise<void> {
|
|
463
563
|
return this.#mutex.run(async () => {
|
|
564
|
+
// One last try to save what the disk refused earlier.
|
|
565
|
+
if (this.#pending.length > 0) await this.#flushPending().catch(() => undefined)
|
|
464
566
|
if (this.#held?.fence === lease.fence) {
|
|
465
567
|
this.#held = undefined
|
|
466
568
|
untrackHeldSessionLease(this.#holding)
|
|
@@ -655,13 +757,126 @@ export class SessionLogCore implements SessionLog {
|
|
|
655
757
|
}
|
|
656
758
|
const bytes = Buffer.from(line, 'utf8')
|
|
657
759
|
const sync = this.#sync === 'all' || (this.#sync === 'boundaries' && BOUNDARY_TYPES.has(type))
|
|
658
|
-
await this.#
|
|
760
|
+
await this.#persist(bytes, sync)
|
|
659
761
|
const entry = this.#chain.accept(bytes, this.#synced)
|
|
660
762
|
this.#turns.apply(entry.record)
|
|
661
763
|
this.#synced += bytes.byteLength
|
|
662
764
|
return entry
|
|
663
765
|
}
|
|
664
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Hand a record's bytes to the medium. A volume that refuses writes (full,
|
|
769
|
+
* over quota, read-only) does not fail the record: it waits in a bounded
|
|
770
|
+
* buffer and goes out, in order, with the next record that can be written.
|
|
771
|
+
* Any other error is thrown as before. Caller holds the mutex.
|
|
772
|
+
*/
|
|
773
|
+
async #persist(bytes: Buffer, sync: boolean): Promise<void> {
|
|
774
|
+
if (this.#pending.length === 0) {
|
|
775
|
+
try {
|
|
776
|
+
await this.#medium.append(bytes, this.#synced, sync)
|
|
777
|
+
return
|
|
778
|
+
} catch (error) {
|
|
779
|
+
const cause = sessionLogDegradedCause(error)
|
|
780
|
+
if (cause === undefined) throw error
|
|
781
|
+
this.#diskBytes = this.#synced
|
|
782
|
+
this.#degradedCause = cause
|
|
783
|
+
this.#bufferRecord(bytes, sync)
|
|
784
|
+
this.#notify({
|
|
785
|
+
state: 'degraded',
|
|
786
|
+
cause,
|
|
787
|
+
bufferedRecords: this.#pending.length,
|
|
788
|
+
bufferedBytes: this.#pendingBytes,
|
|
789
|
+
})
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const queued = this.#bufferRecord(bytes, sync)
|
|
794
|
+
try {
|
|
795
|
+
await this.#flushPending()
|
|
796
|
+
} catch (error) {
|
|
797
|
+
// Only the volume refusing writes keeps the session going; anything else
|
|
798
|
+
// fails this record, which must then not stay queued.
|
|
799
|
+
if (sessionLogDegradedCause(error) === undefined) {
|
|
800
|
+
const at = this.#pending.indexOf(queued)
|
|
801
|
+
if (at !== -1) {
|
|
802
|
+
this.#pending.splice(at, 1)
|
|
803
|
+
this.#pendingBytes -= queued.bytes.byteLength
|
|
804
|
+
}
|
|
805
|
+
throw error
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
#bufferRecord(bytes: Buffer, sync: boolean): { readonly bytes: Buffer; readonly sync: boolean } {
|
|
811
|
+
const queued = { bytes, sync }
|
|
812
|
+
this.#pending.push(queued)
|
|
813
|
+
this.#pendingBytes += bytes.byteLength
|
|
814
|
+
let dropped = false
|
|
815
|
+
// The newest record always stays, whatever the bound.
|
|
816
|
+
while (this.#pendingBytes > this.#degradedBufferBytes && this.#pending.length > 1) {
|
|
817
|
+
const oldest = this.#pending.shift()
|
|
818
|
+
if (oldest === undefined) break
|
|
819
|
+
this.#pendingBytes -= oldest.bytes.byteLength
|
|
820
|
+
this.#droppedRecords += 1
|
|
821
|
+
dropped = true
|
|
822
|
+
}
|
|
823
|
+
if (dropped && !this.#droppedNoticeSent && this.#degradedCause !== undefined) {
|
|
824
|
+
this.#droppedNoticeSent = true
|
|
825
|
+
this.#notify({
|
|
826
|
+
state: 'degraded',
|
|
827
|
+
cause: this.#degradedCause,
|
|
828
|
+
bufferedRecords: this.#pending.length,
|
|
829
|
+
bufferedBytes: this.#pendingBytes,
|
|
830
|
+
dropping: true,
|
|
831
|
+
})
|
|
832
|
+
}
|
|
833
|
+
return queued
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Write the buffer in order. Throws the error that stopped it; clears the degraded mark when it empties. */
|
|
837
|
+
async #flushPending(): Promise<void> {
|
|
838
|
+
// A refused write may have left part of a record behind; cut it back.
|
|
839
|
+
const size = await this.#medium.size()
|
|
840
|
+
if (size > this.#diskBytes) await this.#medium.truncate(this.#diskBytes, size)
|
|
841
|
+
while (this.#pending.length > 0) {
|
|
842
|
+
const next = this.#pending[0]
|
|
843
|
+
if (next === undefined) break
|
|
844
|
+
await this.#medium.append(next.bytes, this.#diskBytes, next.sync)
|
|
845
|
+
this.#diskBytes += next.bytes.byteLength
|
|
846
|
+
this.#pendingBytes -= next.bytes.byteLength
|
|
847
|
+
this.#pending.shift()
|
|
848
|
+
this.#flushedWhileDegraded += 1
|
|
849
|
+
}
|
|
850
|
+
const flushed = this.#flushedWhileDegraded
|
|
851
|
+
this.#flushedWhileDegraded = 0
|
|
852
|
+
const dropped = this.#droppedRecords
|
|
853
|
+
this.#degradedCause = undefined
|
|
854
|
+
this.#droppedRecords = 0
|
|
855
|
+
this.#droppedNoticeSent = false
|
|
856
|
+
this.#pendingBytes = 0
|
|
857
|
+
this.#notify({ state: 'recovered', flushedRecords: flushed, droppedRecords: dropped })
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Write a message body beside the log. When the volume refuses it the body
|
|
862
|
+
* is given up (`undefined`) rather than the record,; the record's own write
|
|
863
|
+
* then meets the same refusal and degrades the log.
|
|
864
|
+
*/
|
|
865
|
+
async #spillOrLose(
|
|
866
|
+
key: string,
|
|
867
|
+
content: Parameters<SpillStore['write']>[1],
|
|
868
|
+
text: string,
|
|
869
|
+
): Promise<SpillRef | undefined> {
|
|
870
|
+
try {
|
|
871
|
+
return await this.#spills.write(key, content, text)
|
|
872
|
+
} catch (error) {
|
|
873
|
+
const cause = sessionLogDegradedCause(error)
|
|
874
|
+
if (cause === undefined) throw error
|
|
875
|
+
// The record's own write, next, finds the volume refusing and degrades the log.
|
|
876
|
+
return undefined
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
665
880
|
async #spillIfLarge(original: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
666
881
|
const candidate = this.#structuredResultSpilling
|
|
667
882
|
? await this.#spillStructuredResults(original)
|
|
@@ -684,11 +899,17 @@ export class SessionLogCore implements SessionLog {
|
|
|
684
899
|
if (size <= spillAbove) return candidate
|
|
685
900
|
if (type === 'turn_completed') {
|
|
686
901
|
if (typeof candidate.result !== 'string') return candidate
|
|
687
|
-
const ref = await this.#
|
|
902
|
+
const ref = await this.#spillOrLose(
|
|
688
903
|
`record:${String(candidate.id)}`,
|
|
689
904
|
'message',
|
|
690
905
|
candidate.result,
|
|
691
906
|
)
|
|
907
|
+
if (ref === undefined) {
|
|
908
|
+
return {
|
|
909
|
+
...candidate,
|
|
910
|
+
result: `${candidate.result.slice(0, PREVIEW_CHARS)}\n${NOT_SAVED_NOTE}`,
|
|
911
|
+
}
|
|
912
|
+
}
|
|
692
913
|
const preview = `${candidate.result.slice(0, PREVIEW_CHARS)}\n[… ${ref.bytes} bytes spilled to ${ref.path}]`
|
|
693
914
|
return { ...candidate, result: preview, resultSpill: ref }
|
|
694
915
|
}
|
|
@@ -708,9 +929,20 @@ export class SessionLogCore implements SessionLog {
|
|
|
708
929
|
: type === 'message_replaced'
|
|
709
930
|
? `record:${String(candidate.id)}`
|
|
710
931
|
: `message:${String(candidate.messageId)}`
|
|
711
|
-
const ref = await this.#
|
|
932
|
+
const ref = await this.#spillOrLose(key, 'message', JSON.stringify(content))
|
|
712
933
|
const text =
|
|
713
934
|
typeof content.content === 'string' ? content.content : JSON.stringify(content.content)
|
|
935
|
+
if (ref === undefined) {
|
|
936
|
+
// The body cannot be kept: the record carries its preview alone.
|
|
937
|
+
return {
|
|
938
|
+
...candidate,
|
|
939
|
+
content: {
|
|
940
|
+
role: content.role,
|
|
941
|
+
content: `${text.slice(0, PREVIEW_CHARS)}\n${NOT_SAVED_NOTE}`,
|
|
942
|
+
...(typeof content.toolCallId === 'string' ? { toolCallId: content.toolCallId } : {}),
|
|
943
|
+
},
|
|
944
|
+
}
|
|
945
|
+
}
|
|
714
946
|
const preview = {
|
|
715
947
|
role: content.role,
|
|
716
948
|
content: `${text.slice(0, PREVIEW_CHARS)}\n[… ${ref.bytes} bytes spilled to ${ref.path}]`,
|
|
@@ -135,7 +135,12 @@ export class DiskLogMedium implements LogMedium {
|
|
|
135
135
|
export interface DiskSessionLogOptions
|
|
136
136
|
extends Pick<
|
|
137
137
|
SessionLogCoreOptions,
|
|
138
|
-
|
|
138
|
+
| 'now'
|
|
139
|
+
| 'spillAboveBytes'
|
|
140
|
+
| 'compactionSpillAboveBytes'
|
|
141
|
+
| 'structuredResultSpilling'
|
|
142
|
+
| 'sync'
|
|
143
|
+
| 'degradedBufferBytes'
|
|
139
144
|
> {
|
|
140
145
|
readonly sessionId: SessionId
|
|
141
146
|
/** `<session-id>.jsonl`. */
|
|
@@ -174,6 +179,7 @@ export class DiskSessionLog extends SessionLogCore {
|
|
|
174
179
|
compactionSpillAboveBytes: options.compactionSpillAboveBytes,
|
|
175
180
|
structuredResultSpilling: options.structuredResultSpilling,
|
|
176
181
|
sync: options.sync,
|
|
182
|
+
degradedBufferBytes: options.degradedBufferBytes,
|
|
177
183
|
})
|
|
178
184
|
this.file = options.file
|
|
179
185
|
this.sessionDir = options.sessionDir
|
|
@@ -21,6 +21,7 @@ export {
|
|
|
21
21
|
type ActiveTurnOptions,
|
|
22
22
|
type AbandonTurnLogOptions,
|
|
23
23
|
type BeginTurnOptions,
|
|
24
|
+
DEFAULT_DEGRADED_BUFFER_BYTES,
|
|
24
25
|
InvalidSessionRecordError,
|
|
25
26
|
type LogMedium,
|
|
26
27
|
type ReadSessionLogOptions,
|
|
@@ -29,10 +30,13 @@ export {
|
|
|
29
30
|
SessionLogConflictError,
|
|
30
31
|
SessionLogCore,
|
|
31
32
|
type SessionLogCoreOptions,
|
|
33
|
+
type SessionLogDegradedCause,
|
|
32
34
|
type SessionLogHead,
|
|
33
35
|
type SessionLogRead,
|
|
34
36
|
type SessionLogReadSummary,
|
|
37
|
+
type SessionLogWriteHealth,
|
|
35
38
|
type SessionRecordDraft,
|
|
39
|
+
sessionLogDegradedCause,
|
|
36
40
|
type TurnStartedDraft,
|
|
37
41
|
walkSessionLog,
|
|
38
42
|
} from './core.js'
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
import { randomUUID } from 'node:crypto'
|
|
19
19
|
import { statSync } from 'node:fs'
|
|
20
20
|
import { rm } from 'node:fs/promises'
|
|
21
|
-
import { tmpdir } from 'node:os'
|
|
22
|
-
import { join } from 'node:path'
|
|
23
21
|
|
|
22
|
+
import { tempFilePath } from '../../utils/temp.js'
|
|
24
23
|
import { type CommandShell, hostCommandShell } from '../command-shell.js'
|
|
25
24
|
import { pathOutsideRoots, toolRoots } from '../paths.js'
|
|
26
25
|
import { readBoundedFile } from './bounded-file-read.js'
|
|
@@ -55,7 +54,7 @@ export function withCwdCapture(command: string, file: string): string {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
export function newCwdFile(): string {
|
|
58
|
-
return
|
|
57
|
+
return tempFilePath(`cwd-${randomUUID()}`)
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
/** Where this session's next foreground call starts. Synchronous: the spawn must not wait on it. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
2
|
+
import { readdir, rm } from 'node:fs/promises'
|
|
4
3
|
import { join } from 'node:path'
|
|
4
|
+
import { createTempDir } from '../../utils/temp.js'
|
|
5
5
|
import { readBoundedFile } from './bounded-file-read.js'
|
|
6
6
|
import { screenshotTargetSize } from './computer-use-image.js'
|
|
7
7
|
import { describeRaster } from './view-image.js'
|
|
@@ -254,7 +254,7 @@ export async function readPdfPages(
|
|
|
254
254
|
}
|
|
255
255
|
last = total === null ? PDF_WHOLE_READ_MAX_PAGES : total
|
|
256
256
|
}
|
|
257
|
-
const directory = await
|
|
257
|
+
const directory = await createTempDir('pdf-')
|
|
258
258
|
try {
|
|
259
259
|
const rendered = await renderPages(bytes, first, last, directory, signal)
|
|
260
260
|
if (!rendered.ok) return { ok: false, message: rendered.message }
|
package/src/turn/reporter.ts
CHANGED
|
@@ -422,6 +422,18 @@ export function createTurnReporter(parentLogger?: Logger): TurnReporter {
|
|
|
422
422
|
})
|
|
423
423
|
break
|
|
424
424
|
|
|
425
|
+
case 'model_refusal':
|
|
426
|
+
log.warn('Model declined to answer', {
|
|
427
|
+
[NAMZU.TURN_ID]: event.turnId,
|
|
428
|
+
[NAMZU.ITERATION]: event.iteration,
|
|
429
|
+
'namzu.turn.from_model': event.model,
|
|
430
|
+
'namzu.turn.from_provider_id': event.providerId,
|
|
431
|
+
'namzu.turn.outcome': event.outcome,
|
|
432
|
+
'namzu.turn.to_model': event.fallbackModel,
|
|
433
|
+
'namzu.turn.category': event.category,
|
|
434
|
+
})
|
|
435
|
+
break
|
|
436
|
+
|
|
425
437
|
case 'provider_fallback':
|
|
426
438
|
// `warn` for the same reason as a retry, and one stronger: the rest
|
|
427
439
|
// of this turn is being served by a provider the caller did not pick.
|
|
@@ -443,6 +455,8 @@ export function createTurnReporter(parentLogger?: Logger): TurnReporter {
|
|
|
443
455
|
case 'tool_calls_admitted':
|
|
444
456
|
case 'compaction_shed':
|
|
445
457
|
case 'compaction_started':
|
|
458
|
+
case 'session_log_degraded':
|
|
459
|
+
case 'session_log_recovered':
|
|
446
460
|
// Deliberately silent. The report is what a human reads about a
|
|
447
461
|
// run; replaying every shed message into it would bury the
|
|
448
462
|
// summary line that says the pass happened, in exactly the
|
package/src/types/agent/query.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface QueryAgentConfig extends BaseAgentConfig {
|
|
|
98
98
|
maxToolConcurrency?: number
|
|
99
99
|
/** See {@link QueryParams.maxOutputContinuations}. */
|
|
100
100
|
maxOutputContinuations?: QueryParams['maxOutputContinuations']
|
|
101
|
+
/** See {@link QueryParams.refusalFallback}. */
|
|
102
|
+
refusalFallback?: QueryParams['refusalFallback']
|
|
101
103
|
maxToolOutputChars?: number
|
|
102
104
|
maxToolOutputCharsPerMessage?: number
|
|
103
105
|
retainedToolPreviewChars?: QueryParams['retainedToolPreviewChars']
|
package/src/types/hitl/index.ts
CHANGED
|
@@ -300,6 +300,14 @@ export interface ToolCallSummary {
|
|
|
300
300
|
* and `strict`. A host's own handler may ignore it.
|
|
301
301
|
*/
|
|
302
302
|
skillGrant?: { readonly skill: string }
|
|
303
|
+
/**
|
|
304
|
+
* Set when this call writes a file inside the session's own scratch folder
|
|
305
|
+
* and nothing else about it needs a person: the folder is the session's, so
|
|
306
|
+
* the review policy does not ask about it (outside `strict`, which asks about
|
|
307
|
+
* everything). A rule that asked for review, a protected path and a tool's
|
|
308
|
+
* own `requiresApproval` all keep the call in review whatever this says.
|
|
309
|
+
*/
|
|
310
|
+
scratchpadWrite?: true
|
|
303
311
|
/**
|
|
304
312
|
* What a `pre_tool_use` hook answered about approving this call, as the
|
|
305
313
|
* hooks reported it. `ask` is acted on by the kernel (it makes the call an
|
|
@@ -367,6 +367,7 @@ export const RUNTIME_CONTEXT_MESSAGE_KINDS = [
|
|
|
367
367
|
'peer-notice',
|
|
368
368
|
'reminder',
|
|
369
369
|
'repeat-call',
|
|
370
|
+
'refusal-retry',
|
|
370
371
|
'steering',
|
|
371
372
|
'step-context',
|
|
372
373
|
'structured-output',
|
|
@@ -695,9 +696,10 @@ export function createTurnContextEntry(
|
|
|
695
696
|
}
|
|
696
697
|
|
|
697
698
|
/** The key a recorded context entry is filed under, or undefined for any other message. */
|
|
698
|
-
export function turnContextKey(message: {
|
|
699
|
-
|
|
700
|
-
|
|
699
|
+
export function turnContextKey(message: {
|
|
700
|
+
readonly role: string
|
|
701
|
+
readonly source?: unknown
|
|
702
|
+
}): string | undefined {
|
|
701
703
|
if (message.role !== 'user') return undefined
|
|
702
704
|
const source = message.source as RuntimeContextMessageSource | undefined
|
|
703
705
|
return source?.type === 'runtime-context' && source.kind === 'turn-context'
|
|
@@ -184,6 +184,24 @@ export type ReasoningEffort =
|
|
|
184
184
|
| 'max'
|
|
185
185
|
| 'ultra'
|
|
186
186
|
|
|
187
|
+
/**
|
|
188
|
+
* What a provider says about a reply it declined to give.
|
|
189
|
+
*
|
|
190
|
+
* Carried beside `finishReason: 'content_filter'` and set whenever the finish
|
|
191
|
+
* was a refusal or a safety block, so a host can tell a model that declined
|
|
192
|
+
* from a reply that was merely empty. Every field is optional because
|
|
193
|
+
* providers differ in what they report and several report a refusal with no
|
|
194
|
+
* detail at all: an empty object means "refused, nothing more is known".
|
|
195
|
+
*/
|
|
196
|
+
export interface ProviderRefusal {
|
|
197
|
+
/** The provider's own classification of why, when it gives one (for example `cyber`). */
|
|
198
|
+
category?: string
|
|
199
|
+
/** The provider's own sentence about the refusal. Show it; do not parse it. */
|
|
200
|
+
explanation?: string
|
|
201
|
+
/** A model the provider says can usually serve the request instead. */
|
|
202
|
+
recommendedModel?: string
|
|
203
|
+
}
|
|
204
|
+
|
|
187
205
|
export interface ChatCompletionResponse {
|
|
188
206
|
id: string
|
|
189
207
|
model: string
|
|
@@ -206,5 +224,7 @@ export interface ChatCompletionResponse {
|
|
|
206
224
|
* window rather than the output token limit; see `StreamChunk.finishDetail`.
|
|
207
225
|
*/
|
|
208
226
|
finishDetail?: 'context_window'
|
|
227
|
+
/** Set with `finishReason: 'content_filter'` when the provider declined to answer. */
|
|
228
|
+
refusal?: ProviderRefusal
|
|
209
229
|
usage: TokenUsage
|
|
210
230
|
}
|
|
@@ -72,6 +72,8 @@ export interface MockTurn {
|
|
|
72
72
|
/** Passages this scripted turn cites, emitted before its text. */
|
|
73
73
|
citations?: import('../message/index.js').Citation[]
|
|
74
74
|
finishReason?: 'stop' | 'tool_calls' | 'length' | 'content_filter'
|
|
75
|
+
/** What the provider says about a `'content_filter'` finish. Defaults to `{}`. */
|
|
76
|
+
refusal?: import('./chat.js').ProviderRefusal
|
|
75
77
|
usage?: Partial<import('../common/index.js').TokenUsage>
|
|
76
78
|
/** Text fragment size. */
|
|
77
79
|
chunkSize?: number
|
|
@@ -106,6 +108,11 @@ export interface MockScript {
|
|
|
106
108
|
* registry-level declaration cannot express "a driver with no vision".
|
|
107
109
|
*/
|
|
108
110
|
capabilities?: ProviderCapabilities
|
|
111
|
+
/**
|
|
112
|
+
* The model this provider names when asked where to re-run a refused
|
|
113
|
+
* request (`LLMProvider.refusalFallbackFor`). Absent means it names none.
|
|
114
|
+
*/
|
|
115
|
+
refusalFallbackModel?: string
|
|
109
116
|
/** Observe every request (assert on tools, toolChoice, cacheControl…). */
|
|
110
117
|
onRequest?: (params: import('./chat.js').ChatCompletionParams) => void
|
|
111
118
|
}
|
|
@@ -236,4 +236,24 @@ export interface LLMProvider {
|
|
|
236
236
|
* run that would otherwise work.
|
|
237
237
|
*/
|
|
238
238
|
resolveContextWindow?(model: string, signal?: AbortSignal): Promise<number | undefined>
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The model to re-run a refused request on, when this provider has one.
|
|
242
|
+
*
|
|
243
|
+
* Called after `model` declined to answer (`finishReason: 'content_filter'`),
|
|
244
|
+
* with whatever the provider said about it. Answer with a different model
|
|
245
|
+
* this provider and this credential can actually use, or `undefined` when
|
|
246
|
+
* there is none: the turn then does not change model. A driver whose
|
|
247
|
+
* service names a recommended model in `refusal.recommendedModel` should
|
|
248
|
+
* use it only after checking it is one this account can serve, and falls
|
|
249
|
+
* back to its own declared order otherwise.
|
|
250
|
+
*
|
|
251
|
+
* Optional. A provider without it never has its model swapped on a
|
|
252
|
+
* refusal. A request that failed for any other reason does not call this.
|
|
253
|
+
*/
|
|
254
|
+
refusalFallbackFor?(
|
|
255
|
+
model: string,
|
|
256
|
+
refusal: import('./chat.js').ProviderRefusal,
|
|
257
|
+
signal?: AbortSignal,
|
|
258
|
+
): Promise<string | undefined>
|
|
239
259
|
}
|
|
@@ -132,6 +132,12 @@ export interface StreamChunk {
|
|
|
132
132
|
* room to continue into. Set only with `finishReason: 'length'`.
|
|
133
133
|
*/
|
|
134
134
|
finishDetail?: 'context_window'
|
|
135
|
+
/**
|
|
136
|
+
* The provider declined to answer. Set with `finishReason: 'content_filter'`
|
|
137
|
+
* by every driver whose finish was a refusal or a safety block; an empty
|
|
138
|
+
* object when the provider gave no detail.
|
|
139
|
+
*/
|
|
140
|
+
refusal?: import('./chat.js').ProviderRefusal
|
|
135
141
|
usage?: TokenUsage
|
|
136
142
|
error?: string
|
|
137
143
|
|