@namzu/sdk 59.0.0 → 60.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 +102 -0
- package/dist/compaction/context-window.d.ts +53 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +141 -2
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/execution/workflow/index.d.ts +3 -1
- package/dist/execution/workflow/index.d.ts.map +1 -1
- package/dist/execution/workflow/index.js +2 -1
- package/dist/execution/workflow/index.js.map +1 -1
- package/dist/execution/workflow/journal.d.ts +24 -4
- package/dist/execution/workflow/journal.d.ts.map +1 -1
- package/dist/execution/workflow/journal.js +131 -9
- package/dist/execution/workflow/journal.js.map +1 -1
- package/dist/execution/workflow/prelude.d.ts +2 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +49 -8
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/execution/workflow/resume.d.ts +59 -0
- package/dist/execution/workflow/resume.d.ts.map +1 -0
- package/dist/execution/workflow/resume.js +273 -0
- package/dist/execution/workflow/resume.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +112 -8
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +105 -1
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin/compat.d.ts +24 -0
- package/dist/plugin/compat.d.ts.map +1 -0
- package/dist/plugin/compat.js +149 -0
- package/dist/plugin/compat.js.map +1 -0
- package/dist/plugin/loader.d.ts +2 -1
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +6 -0
- package/dist/plugin/loader.js.map +1 -1
- package/dist/pricing/index.d.ts.map +1 -1
- package/dist/pricing/index.js +2 -1
- package/dist/pricing/index.js.map +1 -1
- package/dist/process/kill-tree.d.ts +44 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +102 -4
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +9 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts +18 -0
- package/dist/provider/model-window-suffix.d.ts.map +1 -0
- package/dist/provider/model-window-suffix.js +77 -0
- package/dist/provider/model-window-suffix.js.map +1 -0
- package/dist/public-runtime.d.ts +5 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +1 -4
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +0 -28
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.js +11 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
- package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
- package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
- package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
- package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
- package/dist/runtime/jobs/liveness.d.ts +67 -0
- package/dist/runtime/jobs/liveness.d.ts.map +1 -0
- package/dist/runtime/jobs/liveness.js +94 -0
- package/dist/runtime/jobs/liveness.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +104 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/jobs/wake-budget.d.ts +35 -0
- package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
- package/dist/runtime/jobs/wake-budget.js +48 -0
- package/dist/runtime/jobs/wake-budget.js.map +1 -0
- package/dist/runtime/query/detached-calls.d.ts +65 -0
- package/dist/runtime/query/detached-calls.d.ts.map +1 -0
- package/dist/runtime/query/detached-calls.js +116 -0
- package/dist/runtime/query/detached-calls.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +4 -1
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +90 -6
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +20 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +31 -5
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +11 -3
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +7 -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 +63 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +8 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +29 -3
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +58 -3
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -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/runtime/system-events.d.ts +5 -0
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +7 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/denial.d.ts +11 -0
- package/dist/sandbox/denial.d.ts.map +1 -0
- package/dist/sandbox/denial.js +61 -0
- package/dist/sandbox/denial.js.map +1 -0
- package/dist/sandbox/file-walk-program.d.ts.map +1 -1
- package/dist/sandbox/file-walk-program.js +31 -13
- package/dist/sandbox/file-walk-program.js.map +1 -1
- package/dist/sandbox/file-walk.d.ts +2 -0
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +64 -13
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/ignore-matcher.d.ts +30 -0
- package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
- package/dist/sandbox/ignore-matcher.js +11 -0
- package/dist/sandbox/ignore-matcher.js.map +1 -0
- package/dist/sandbox/ignore-source.d.ts +26 -0
- package/dist/sandbox/ignore-source.d.ts.map +1 -0
- package/dist/sandbox/ignore-source.js +221 -0
- package/dist/sandbox/ignore-source.js.map +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +11 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +1 -0
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/session/log-hash.d.ts.map +1 -1
- package/dist/session/log-hash.js +18 -1
- package/dist/session/log-hash.js.map +1 -1
- package/dist/store/session-index/sqlite.d.ts.map +1 -1
- package/dist/store/session-index/sqlite.js +169 -16
- package/dist/store/session-index/sqlite.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +10 -4
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +35 -0
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +512 -67
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/glob.d.ts +1 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +65 -28
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +94 -40
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/job.d.ts +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +34 -7
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +44 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -0
- package/dist/tools/builtins/search-support.js +89 -0
- package/dist/tools/builtins/search-support.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +13 -2
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +14 -5
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/computer-use/index.d.ts +129 -13
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/plugin/index.d.ts +24 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +4 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +38 -1
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/session/records.d.ts +264 -0
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +32 -1
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +8 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/compaction/context-window.ts +163 -2
- package/src/execution/workflow/index.ts +14 -1
- package/src/execution/workflow/journal.ts +161 -11
- package/src/execution/workflow/prelude.ts +51 -8
- package/src/execution/workflow/resume.ts +313 -0
- package/src/execution/workflow/runtime.ts +127 -9
- package/src/execution/workflow/types.ts +109 -1
- package/src/index.ts +17 -0
- package/src/plugin/compat.ts +173 -0
- package/src/plugin/loader.ts +7 -1
- package/src/pricing/index.ts +2 -1
- package/src/process/kill-tree.ts +106 -4
- package/src/prompt/coding-agent-doctrine.ts +9 -1
- package/src/provider/model-window-suffix.ts +84 -0
- package/src/public-runtime.ts +21 -2
- package/src/public-types.ts +1 -0
- package/src/registry/command/kernel-commands.ts +1 -32
- package/src/runtime/jobs/awaited-jobs.ts +11 -0
- package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
- package/src/runtime/jobs/liveness.ts +144 -0
- package/src/runtime/jobs/registry.ts +114 -2
- package/src/runtime/jobs/wake-budget.ts +55 -0
- package/src/runtime/query/detached-calls.ts +135 -0
- package/src/runtime/query/executor.ts +92 -10
- package/src/runtime/query/index.ts +50 -7
- package/src/runtime/query/iteration/outstanding-work.ts +12 -3
- package/src/runtime/query/iteration/phases/context.ts +7 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
- package/src/runtime/query/session-storage.ts +8 -3
- package/src/runtime/query/steering.ts +77 -6
- package/src/runtime/query/tooling.ts +2 -0
- package/src/runtime/system-events.ts +12 -1
- package/src/sandbox/denial.ts +79 -0
- package/src/sandbox/file-walk-program.ts +32 -13
- package/src/sandbox/file-walk.ts +75 -12
- package/src/sandbox/ignore-matcher.ts +46 -0
- package/src/sandbox/ignore-source.ts +220 -0
- package/src/sandbox/provider/local.ts +11 -1
- package/src/scheduler/completion-inbox.ts +1 -0
- package/src/session/log-hash.ts +21 -0
- package/src/store/session-index/sqlite.ts +171 -16
- package/src/tools/builtins/bash.ts +11 -4
- package/src/tools/builtins/computer-use.ts +593 -68
- package/src/tools/builtins/glob.ts +79 -32
- package/src/tools/builtins/grep.ts +107 -43
- package/src/tools/builtins/job.ts +42 -12
- package/src/tools/builtins/search-support.ts +107 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
- package/src/tools/builtins/wait-for-job.ts +14 -2
- package/src/tools/command-shell.ts +13 -5
- package/src/types/computer-use/index.ts +157 -15
- package/src/types/plugin/index.ts +8 -0
- package/src/types/sandbox/index.ts +40 -1
- package/src/types/session/records.ts +36 -2
- package/src/types/tool/index.ts +30 -0
- package/src/types/tool/presentation.ts +8 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { spawn } from 'node:child_process'
|
|
2
2
|
|
|
3
3
|
import { SANDBOX_KILL_GRACE_MS } from '../../constants/sandbox/index.js'
|
|
4
|
-
import { killTree } from '../../process/kill-tree.js'
|
|
4
|
+
import { isSignalablePid, killTree } from '../../process/kill-tree.js'
|
|
5
5
|
import { describeSpawnFailure, spawnHostShell } from '../../tools/command-shell.js'
|
|
6
6
|
import { scrubInheritedEnv } from '../../tools/env-scrub.js'
|
|
7
7
|
import { createHostOutputDecoder } from '../../tools/host-output.js'
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
import type { MonitorWebSocketTarget } from '../../types/tool/index.js'
|
|
13
13
|
import { subscribeToAbort } from '../../utils/abort.js'
|
|
14
14
|
import { oneMetadataLine } from '../system-events.js'
|
|
15
|
+
import { stopAtHostExit } from './host-exit-finalizer.js'
|
|
15
16
|
import { createBackgroundJobId } from './job-id.js'
|
|
16
17
|
import {
|
|
17
18
|
MONITOR_TIMEOUT_CAP_MS,
|
|
@@ -53,6 +54,10 @@ export interface BackgroundJob {
|
|
|
53
54
|
readonly command: string
|
|
54
55
|
readonly status: BackgroundJobStatus
|
|
55
56
|
readonly startedAt: number
|
|
57
|
+
/** When the job last wrote output (epoch ms). Absent until it has written anything. */
|
|
58
|
+
readonly lastOutputAt?: number
|
|
59
|
+
/** The last non-empty output line, flattened and clipped to {@link LAST_OUTPUT_LINE_MAX}. */
|
|
60
|
+
readonly lastOutputLine?: string
|
|
56
61
|
readonly exitedAt?: number
|
|
57
62
|
readonly exitCode?: number
|
|
58
63
|
readonly signal?: string
|
|
@@ -347,7 +352,7 @@ interface MonitorState {
|
|
|
347
352
|
|
|
348
353
|
/** Whether any process of the group led by `pid` is still alive. */
|
|
349
354
|
function groupAlive(pid: number | undefined): boolean {
|
|
350
|
-
if (pid
|
|
355
|
+
if (!isSignalablePid(pid) || process.platform === 'win32') return false
|
|
351
356
|
try {
|
|
352
357
|
process.kill(-pid, 0)
|
|
353
358
|
return true
|
|
@@ -356,8 +361,67 @@ function groupAlive(pid: number | undefined): boolean {
|
|
|
356
361
|
}
|
|
357
362
|
}
|
|
358
363
|
|
|
364
|
+
/**
|
|
365
|
+
* The same probe for a status read: a group that exists but is not ours to
|
|
366
|
+
* signal (EPERM) is there. The close handler keeps {@link groupAlive}, which
|
|
367
|
+
* must not wait on a group it can never see end.
|
|
368
|
+
*
|
|
369
|
+
* Not proof the group is still the job's: between the group ending and the
|
|
370
|
+
* record closing, a recycled pid that leads a new group would read as alive.
|
|
371
|
+
* The close handler polls every {@link GROUP_POLL_MS}, so the window is
|
|
372
|
+
* that small.
|
|
373
|
+
*/
|
|
374
|
+
function groupPresent(pid: number): boolean {
|
|
375
|
+
if (!isSignalablePid(pid)) return false
|
|
376
|
+
try {
|
|
377
|
+
process.kill(-pid, 0)
|
|
378
|
+
return true
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM'
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
359
384
|
const GROUP_POLL_MS = 250
|
|
360
385
|
|
|
386
|
+
/** Longest `lastOutputLine` kept on a job record. */
|
|
387
|
+
export const LAST_OUTPUT_LINE_MAX = 160
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Drop terminal control sequences and control characters, keeping one space
|
|
391
|
+
* where a control character stood. Done by code point so no pattern has to
|
|
392
|
+
* spell a control character.
|
|
393
|
+
*/
|
|
394
|
+
function plainLine(raw: string): string {
|
|
395
|
+
let out = ''
|
|
396
|
+
let i = 0
|
|
397
|
+
while (i < raw.length) {
|
|
398
|
+
const code = raw.charCodeAt(i)
|
|
399
|
+
if (code === 0x1b && raw[i + 1] === '[') {
|
|
400
|
+
// CSI: parameters and intermediates, then one final byte in @..~.
|
|
401
|
+
i += 2
|
|
402
|
+
while (i < raw.length && raw.charCodeAt(i) >= 0x20 && raw.charCodeAt(i) <= 0x3f) i += 1
|
|
403
|
+
i += 1
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
out += code < 0x20 || code === 0x7f ? ' ' : (raw[i] ?? '')
|
|
407
|
+
i += 1
|
|
408
|
+
}
|
|
409
|
+
return out.replace(/ {2,}/g, ' ').trim()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** The last non-empty line of `text`, flattened and clipped; `undefined` when it has none. */
|
|
413
|
+
function lastLineOf(text: string): string | undefined {
|
|
414
|
+
const lines = text.split('\n')
|
|
415
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
416
|
+
const flat = plainLine(lines[index] ?? '')
|
|
417
|
+
if (flat.length > 0)
|
|
418
|
+
return flat.length <= LAST_OUTPUT_LINE_MAX
|
|
419
|
+
? flat
|
|
420
|
+
: `${flat.slice(0, LAST_OUTPUT_LINE_MAX - 1)}…`
|
|
421
|
+
}
|
|
422
|
+
return undefined
|
|
423
|
+
}
|
|
424
|
+
|
|
361
425
|
/**
|
|
362
426
|
* Why a command that was running in the foreground moved to the background:
|
|
363
427
|
* the person asked (`user`), or the person sent a message that has to reach
|
|
@@ -708,6 +772,7 @@ export class BackgroundJobRegistry {
|
|
|
708
772
|
const finalize = (code: number | null, signal: NodeJS.Signals | null): void => {
|
|
709
773
|
if (finalized) return
|
|
710
774
|
finalized = true
|
|
775
|
+
untrackAtHostExit()
|
|
711
776
|
entry.record = {
|
|
712
777
|
...entry.record,
|
|
713
778
|
// A job killed by this registry says `killed`, not
|
|
@@ -788,11 +853,26 @@ export class BackgroundJobRegistry {
|
|
|
788
853
|
}),
|
|
789
854
|
}
|
|
790
855
|
entry.finalize = finalizeEntry
|
|
856
|
+
// A job that is not stopped before the host exits is an orphan: nothing
|
|
857
|
+
// owns its id any more. A remote job confirms its own termination and is
|
|
858
|
+
// not signalled from an exit listener.
|
|
859
|
+
const untrackAtHostExit = entry.terminateProcess
|
|
860
|
+
? () => {}
|
|
861
|
+
: stopAtHostExit({
|
|
862
|
+
child,
|
|
863
|
+
...(entry.killProcess ? { kill: entry.killProcess } : {}),
|
|
864
|
+
})
|
|
791
865
|
|
|
792
866
|
const append = (text: string, stream: 'stdout' | 'stderr'): void => {
|
|
793
867
|
const chunk: OutputChunk = { stream, offset: entry.produced, bytes: Buffer.from(text) }
|
|
794
868
|
if (chunk.bytes.length === 0) return
|
|
795
869
|
entry.produced += chunk.bytes.length
|
|
870
|
+
const line = lastLineOf(text)
|
|
871
|
+
entry.record = {
|
|
872
|
+
...entry.record,
|
|
873
|
+
lastOutputAt: Date.now(),
|
|
874
|
+
...(line !== undefined ? { lastOutputLine: line } : {}),
|
|
875
|
+
}
|
|
796
876
|
entry.buffer += text
|
|
797
877
|
entry.buffer = outputTail(entry.buffer, this.maxBytes).text
|
|
798
878
|
entry.outputChunks.push(chunk)
|
|
@@ -898,6 +978,20 @@ export class BackgroundJobRegistry {
|
|
|
898
978
|
return entry.record
|
|
899
979
|
}
|
|
900
980
|
|
|
981
|
+
/**
|
|
982
|
+
* Whether the job's process group is still there, asked of the system rather
|
|
983
|
+
* than inferred from the record. A job with no process of its own to probe
|
|
984
|
+
* (a sandboxed or WebSocket source, or a platform without process groups)
|
|
985
|
+
* is alive exactly when the registry says it is running.
|
|
986
|
+
*/
|
|
987
|
+
isAlive(id: string): boolean {
|
|
988
|
+
const entry = this.jobs.get(id)
|
|
989
|
+
if (!entry || entry.record.status !== 'running') return false
|
|
990
|
+
const pid = entry.child.pid
|
|
991
|
+
if (pid === undefined || process.platform === 'win32') return true
|
|
992
|
+
return groupPresent(pid)
|
|
993
|
+
}
|
|
994
|
+
|
|
901
995
|
/**
|
|
902
996
|
* Await one job's exit — the public counterpart to `onExit`, for a
|
|
903
997
|
* caller that wants a single result rather than a standing
|
|
@@ -1253,6 +1347,12 @@ export function bindOwner(
|
|
|
1253
1347
|
* than present and silently doing nothing.
|
|
1254
1348
|
*/
|
|
1255
1349
|
readonly onAwaited?: (id: string) => void
|
|
1350
|
+
/**
|
|
1351
|
+
* Be told that a tool result has handed the model this job's exit, so the
|
|
1352
|
+
* notice announcing the same exit can be withdrawn. Like `onAwaited` it
|
|
1353
|
+
* belongs to the turn; absent, `markCollected` is absent from the ref.
|
|
1354
|
+
*/
|
|
1355
|
+
readonly onCollected?: (id: string) => void
|
|
1256
1356
|
/**
|
|
1257
1357
|
* The longest a monitor started through this binding may live. A host
|
|
1258
1358
|
* may lower it (a single-shot run has no tomorrow to watch for); the
|
|
@@ -1387,11 +1487,23 @@ export function bindOwner(
|
|
|
1387
1487
|
},
|
|
1388
1488
|
}
|
|
1389
1489
|
: {}),
|
|
1490
|
+
...(defaults.onCollected
|
|
1491
|
+
? {
|
|
1492
|
+
markCollected: (id: string) => {
|
|
1493
|
+
mine(id)
|
|
1494
|
+
defaults.onCollected?.(id)
|
|
1495
|
+
},
|
|
1496
|
+
}
|
|
1497
|
+
: {}),
|
|
1390
1498
|
kill: async (id: string) => {
|
|
1391
1499
|
mine(id)
|
|
1392
1500
|
return await registry.kill(id)
|
|
1393
1501
|
},
|
|
1394
1502
|
list: () => registry.list(owner),
|
|
1503
|
+
isAlive: (id: string) => {
|
|
1504
|
+
mine(id)
|
|
1505
|
+
return registry.isAlive(id)
|
|
1506
|
+
},
|
|
1395
1507
|
}
|
|
1396
1508
|
}
|
|
1397
1509
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bound on how many times in a row a session wakes the model on its own.
|
|
3
|
+
*
|
|
4
|
+
* A finished background job wakes an idle session, and the turn that wakes may
|
|
5
|
+
* start another job whose exit wakes it again, with nobody reading. For an
|
|
6
|
+
* owner that nobody is watching, that chain never has to end. The budget caps
|
|
7
|
+
* it: after `max` wakes in a row no further exit wakes the model, and the
|
|
8
|
+
* exits stay pending so the next message from a person carries them.
|
|
9
|
+
*
|
|
10
|
+
* **Off unless asked.** An interactive session has a person to stop it, and a
|
|
11
|
+
* cap there would only get in the way. `max` of `undefined` (or anything that
|
|
12
|
+
* is not a positive integer) never limits.
|
|
13
|
+
*
|
|
14
|
+
* **Only a person resets it.** A wake, a job notice or a tool result is not
|
|
15
|
+
* evidence anyone is looking; a human message is.
|
|
16
|
+
*/
|
|
17
|
+
export class WakeBudget {
|
|
18
|
+
private woken = 0
|
|
19
|
+
private readonly max: number | undefined
|
|
20
|
+
|
|
21
|
+
constructor(max?: number) {
|
|
22
|
+
this.max = max !== undefined && Number.isInteger(max) && max > 0 ? max : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Whether another automatic wake is allowed. */
|
|
26
|
+
get canWake(): boolean {
|
|
27
|
+
return this.max === undefined || this.woken < this.max
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Count one automatic wake. Call when a wake begins a turn. */
|
|
31
|
+
recordWake(): void {
|
|
32
|
+
this.woken++
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A person wrote: the chain is attended again. */
|
|
36
|
+
reset(): void {
|
|
37
|
+
this.woken = 0
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Wakes since the last human message. */
|
|
41
|
+
get consecutiveWakes(): number {
|
|
42
|
+
return this.woken
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The budget a host asked for through `NAMZU_MAX_CONSECUTIVE_WAKES`, or an
|
|
48
|
+
* unlimited one. Anything but a positive integer means no limit.
|
|
49
|
+
*/
|
|
50
|
+
export function wakeBudgetFromEnv(
|
|
51
|
+
env: Record<string, string | undefined> = process.env,
|
|
52
|
+
): WakeBudget {
|
|
53
|
+
const raw = env.NAMZU_MAX_CONSECUTIVE_WAKES?.trim()
|
|
54
|
+
return new WakeBudget(raw && /^\d+$/.test(raw) ? Number(raw) : undefined)
|
|
55
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { frameNotice } from '../../types/message/notice-frame.js'
|
|
2
|
+
import type { ToolCallOutcome } from './executor.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tool calls that were still running when the operator wrote.
|
|
6
|
+
*
|
|
7
|
+
* A model cannot answer while a `tool_use` block has no `tool_result`, and a
|
|
8
|
+
* long call used to hold the operator's question until it finished: the person
|
|
9
|
+
* asked how things stand and heard nothing for as long as the call took. When
|
|
10
|
+
* the operator writes while a call has been running for a while, the batch
|
|
11
|
+
* answers the unfinished calls with a placeholder result, the model reads the
|
|
12
|
+
* message at once, and the call carries on untouched. Its real result reaches
|
|
13
|
+
* the model later as a message of its own that names the call.
|
|
14
|
+
*
|
|
15
|
+
* This holds the bookkeeping for one turn: which calls were handed back this
|
|
16
|
+
* way, and the outcomes that have arrived since.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** How long a call must have been running before an operator message detaches it. */
|
|
20
|
+
export const DETACH_AFTER_MS = 2_000
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What a model reads as the result of a call that is still running.
|
|
24
|
+
*
|
|
25
|
+
* It says the call goes on, what not to do about it, and what to tell the
|
|
26
|
+
* person; the model never sees these words as anything the tool printed.
|
|
27
|
+
*/
|
|
28
|
+
export function stillRunningResult(toolName: string, started = true): string {
|
|
29
|
+
if (!started) {
|
|
30
|
+
return [
|
|
31
|
+
`[Waiting to start] The ${toolName} call has not started: it is queued behind an earlier call in this batch. The operator wrote a message while the batch ran, and that message follows so you can answer it now.`,
|
|
32
|
+
'The call will start by itself when the earlier one ends, and its result will arrive in a later message that names this call id.',
|
|
33
|
+
'Repeating the call would run the work twice, and waiting or polling for it would delay your answer, so do neither. If they ask, say it has not started yet.',
|
|
34
|
+
].join('\n')
|
|
35
|
+
}
|
|
36
|
+
return [
|
|
37
|
+
`[Still running] The ${toolName} call has not finished. The operator wrote a message while it ran, and that message follows so you can answer it now.`,
|
|
38
|
+
'The call was not interrupted. Its result will arrive on its own, in a later message that names this call id.',
|
|
39
|
+
'Repeating the call would run the work twice, and waiting or polling for it would delay your answer, so do neither.',
|
|
40
|
+
'You cannot stop it from here, so do not say you cancelled or dropped it. On the operator’s screen it still shows as in progress; if they ask, say it is still running.',
|
|
41
|
+
].join('\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** What a model reads when a call it was told was still running has finished. */
|
|
45
|
+
export function formatLateResults(outcomes: readonly ToolCallOutcome[]): string {
|
|
46
|
+
const parts = outcomes.map(
|
|
47
|
+
(outcome) =>
|
|
48
|
+
`The ${outcome.toolName} call with id ${outcome.toolCallId}, which you were told was still running, has finished${outcome.isError ? ' with an error' : ''}. This is what it returned:\n${outcome.output}`,
|
|
49
|
+
)
|
|
50
|
+
return frameNotice('job', '[Tool call finished]', parts.join('\n\n'))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class DetachedCalls {
|
|
54
|
+
private readonly open = new Map<string, string>()
|
|
55
|
+
private arrived: ToolCallOutcome[] = []
|
|
56
|
+
private readonly waiters = new Set<() => void>()
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `waitForOperator` resolves when the operator wrote and at no other wake: a
|
|
60
|
+
* child's notice or a peer's mail must not hand a call back, because the
|
|
61
|
+
* model would then get a turn with no question to answer.
|
|
62
|
+
*/
|
|
63
|
+
constructor(readonly waitForOperator?: (signal: AbortSignal) => Promise<void>) {}
|
|
64
|
+
|
|
65
|
+
/** A call was answered with the placeholder and is still running. */
|
|
66
|
+
begin(toolCallId: string, toolName: string): void {
|
|
67
|
+
this.open.set(toolCallId, toolName)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The real outcome of a detached call. Ignored for a call that was never detached. */
|
|
71
|
+
settle(outcome: ToolCallOutcome): void {
|
|
72
|
+
if (!this.open.delete(outcome.toolCallId)) return
|
|
73
|
+
this.arrived.push(outcome)
|
|
74
|
+
this.wake()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** True while a call is still running or its outcome has not been read. */
|
|
78
|
+
get hasWork(): boolean {
|
|
79
|
+
return this.open.size > 0 || this.arrived.length > 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Calls still running. */
|
|
83
|
+
get running(): number {
|
|
84
|
+
return this.open.size
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The outcomes that have arrived since the last read, in arrival order. */
|
|
88
|
+
take(): readonly ToolCallOutcome[] {
|
|
89
|
+
const taken = this.arrived
|
|
90
|
+
this.arrived = []
|
|
91
|
+
return taken
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolves when an outcome is waiting, when nothing is running, after
|
|
96
|
+
* `graceMs`, or rejects when `signal` aborts. Resolving early is never
|
|
97
|
+
* delivery: the caller reads with {@link take}.
|
|
98
|
+
*/
|
|
99
|
+
waitForArrival(graceMs: number, signal: AbortSignal): Promise<void> {
|
|
100
|
+
if (this.arrived.length > 0 || this.open.size === 0) return Promise.resolve()
|
|
101
|
+
if (signal.aborted) return Promise.reject(signal.reason)
|
|
102
|
+
return new Promise<void>((resolve, reject) => {
|
|
103
|
+
const done = (): void => {
|
|
104
|
+
clearTimeout(timer)
|
|
105
|
+
this.waiters.delete(wake)
|
|
106
|
+
signal.removeEventListener('abort', abort)
|
|
107
|
+
}
|
|
108
|
+
const wake = (): void => {
|
|
109
|
+
done()
|
|
110
|
+
resolve()
|
|
111
|
+
}
|
|
112
|
+
const abort = (): void => {
|
|
113
|
+
done()
|
|
114
|
+
reject(signal.reason)
|
|
115
|
+
}
|
|
116
|
+
const timer = setTimeout(wake, Math.max(0, graceMs))
|
|
117
|
+
this.waiters.add(wake)
|
|
118
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private wake(): void {
|
|
123
|
+
for (const wake of [...this.waiters]) wake()
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* What one batch needs to hand its unfinished calls back. `requested` settles
|
|
129
|
+
* once the operator has written and the batch has run for {@link DETACH_AFTER_MS};
|
|
130
|
+
* `calls` receives each call that was handed back and, later, its outcome.
|
|
131
|
+
*/
|
|
132
|
+
export interface BatchDetach {
|
|
133
|
+
readonly requested: Promise<void>
|
|
134
|
+
readonly calls: DetachedCalls
|
|
135
|
+
}
|
|
@@ -71,6 +71,7 @@ import type { Logger } from '../../utils/logger.js'
|
|
|
71
71
|
import { compressShellOutput } from '../../utils/shell-compress.js'
|
|
72
72
|
import { parseStructuredResultJson } from '../../utils/structured-result-json.js'
|
|
73
73
|
import { type BackgroundJobRegistry, type JobProcess, bindOwner } from '../jobs/registry.js'
|
|
74
|
+
import { type BatchDetach, stillRunningResult } from './detached-calls.js'
|
|
74
75
|
import {
|
|
75
76
|
type ToolAdmissionHost,
|
|
76
77
|
formatFailedToolOutput,
|
|
@@ -412,6 +413,8 @@ export interface ToolExecutorConfig {
|
|
|
412
413
|
* recorder gets no hold rather than a marking call that goes nowhere.
|
|
413
414
|
*/
|
|
414
415
|
onJobAwaited?: (id: string) => void
|
|
416
|
+
/** Where a tool result that handed the model a job's exit says so; see `ToolContext.backgroundJobs.markCollected`. */
|
|
417
|
+
onJobCollected?: (id: string) => void
|
|
415
418
|
/** The longest a monitor may live in this turn; see `QueryParams.monitorTimeoutCapMs`. */
|
|
416
419
|
monitorTimeoutCapMs?: number
|
|
417
420
|
/**
|
|
@@ -1429,6 +1432,7 @@ export class ToolExecutor {
|
|
|
1429
1432
|
preparedBatch?: PreparedToolBatch,
|
|
1430
1433
|
declines?: ToolCallDeclines,
|
|
1431
1434
|
review?: ReviewedExecution,
|
|
1435
|
+
detach?: BatchDetach,
|
|
1432
1436
|
): Promise<ToolExecutionBatch> {
|
|
1433
1437
|
const toolCalls = response.message.toolCalls
|
|
1434
1438
|
if (!toolCalls) {
|
|
@@ -1455,9 +1459,9 @@ export class ToolExecutor {
|
|
|
1455
1459
|
if (refusal) {
|
|
1456
1460
|
const refused = new Map(denials)
|
|
1457
1461
|
for (const call of toolCalls) if (!refused.has(call.id)) refused.set(call.id, refusal)
|
|
1458
|
-
return await this.runBatch(toolCalls, refused, prior, owned, declines, review)
|
|
1462
|
+
return await this.runBatch(toolCalls, refused, prior, owned, declines, review, detach)
|
|
1459
1463
|
}
|
|
1460
|
-
return await this.runBatch(toolCalls, denials, prior, owned, declines, review)
|
|
1464
|
+
return await this.runBatch(toolCalls, denials, prior, owned, declines, review, detach)
|
|
1461
1465
|
} finally {
|
|
1462
1466
|
// Cleared so a later single execution outside a batch resolves
|
|
1463
1467
|
// live rather than inheriting the last batch's sample.
|
|
@@ -1472,6 +1476,7 @@ export class ToolExecutor {
|
|
|
1472
1476
|
preparedBatch?: OwnedPreparedToolBatch,
|
|
1473
1477
|
declines?: ToolCallDeclines,
|
|
1474
1478
|
review?: ReviewedExecution,
|
|
1479
|
+
detach?: BatchDetach,
|
|
1475
1480
|
): Promise<ToolExecutionBatch> {
|
|
1476
1481
|
this.log.debug('Executing tool batch', {
|
|
1477
1482
|
[NAMZU.TURN_ID]: this.config.turnId,
|
|
@@ -1507,6 +1512,17 @@ export class ToolExecutor {
|
|
|
1507
1512
|
// (which let the last writer clobber the rest). Results are written by
|
|
1508
1513
|
// index to preserve the original tool-call order.
|
|
1509
1514
|
const results: ToolCallOutcome[] = new Array(toolCalls.length)
|
|
1515
|
+
// Calls handed back to the model as still running (see `detach`), and the
|
|
1516
|
+
// ones among them whose real outcome has been reported.
|
|
1517
|
+
const handedBack = new Set<string>()
|
|
1518
|
+
const reported = new Set<string>()
|
|
1519
|
+
// Calls that have begun; one queued behind a serial or barrier call has not.
|
|
1520
|
+
const started = new Set<string>()
|
|
1521
|
+
const reportLate = (outcome: ToolCallOutcome): void => {
|
|
1522
|
+
if (reported.has(outcome.toolCallId)) return
|
|
1523
|
+
reported.add(outcome.toolCallId)
|
|
1524
|
+
detach?.calls.settle(outcome)
|
|
1525
|
+
}
|
|
1510
1526
|
const parallel: Promise<void>[] = []
|
|
1511
1527
|
let serial: Promise<void> = Promise.resolve()
|
|
1512
1528
|
let barrier: Promise<void> | undefined
|
|
@@ -1623,13 +1639,32 @@ export class ToolExecutor {
|
|
|
1623
1639
|
}
|
|
1624
1640
|
const run = async () => {
|
|
1625
1641
|
try {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1642
|
+
let outcome: ToolCallOutcome
|
|
1643
|
+
started.add(toolCall.id)
|
|
1644
|
+
try {
|
|
1645
|
+
outcome = await this.executeSingle(
|
|
1646
|
+
toolCall,
|
|
1647
|
+
ctx,
|
|
1648
|
+
recordObservation,
|
|
1649
|
+
() => progress.close(),
|
|
1650
|
+
preparedCall,
|
|
1651
|
+
)
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
// A call already answered with the placeholder has no batch
|
|
1654
|
+
// left to fail; its failure is its outcome.
|
|
1655
|
+
if (!handedBack.has(toolCall.id)) throw error
|
|
1656
|
+
reportLate({
|
|
1657
|
+
toolCallId: toolCall.id,
|
|
1658
|
+
toolName: toolCall.function.name,
|
|
1659
|
+
output: `Error: Tool "${toolCall.function.name}" did not complete — ${
|
|
1660
|
+
error instanceof Error ? error.message : String(error)
|
|
1661
|
+
}`,
|
|
1662
|
+
isError: true,
|
|
1663
|
+
})
|
|
1664
|
+
return
|
|
1665
|
+
}
|
|
1666
|
+
if (handedBack.has(toolCall.id)) reportLate(outcome)
|
|
1667
|
+
else results[i] = outcome
|
|
1633
1668
|
} finally {
|
|
1634
1669
|
await progress.close()
|
|
1635
1670
|
}
|
|
@@ -1664,7 +1699,46 @@ export class ToolExecutor {
|
|
|
1664
1699
|
schedule(safe ? gated : run, safe, isBarrier)
|
|
1665
1700
|
})
|
|
1666
1701
|
if (review) await this.walkReviewed(reviewUnits, results, review)
|
|
1667
|
-
|
|
1702
|
+
const completion = Promise.all([...parallel, serial])
|
|
1703
|
+
if (detach && !review) {
|
|
1704
|
+
const first = await Promise.race([
|
|
1705
|
+
completion.then(() => 'finished' as const),
|
|
1706
|
+
detach.requested.then(() => 'requested' as const),
|
|
1707
|
+
])
|
|
1708
|
+
if (first === 'requested' && toolCalls.some((_, i) => results[i] === undefined)) {
|
|
1709
|
+
// The operator wrote while calls were still running. Answer each
|
|
1710
|
+
// unfinished call with the placeholder so the model can read the
|
|
1711
|
+
// message now; the calls keep running and report themselves.
|
|
1712
|
+
toolCalls.forEach((toolCall, i) => {
|
|
1713
|
+
if (results[i]) return
|
|
1714
|
+
handedBack.add(toolCall.id)
|
|
1715
|
+
detach.calls.begin(toolCall.id, toolCall.function.name)
|
|
1716
|
+
results[i] = {
|
|
1717
|
+
toolCallId: toolCall.id,
|
|
1718
|
+
toolName: toolCall.function.name,
|
|
1719
|
+
output: stillRunningResult(toolCall.function.name, started.has(toolCall.id)),
|
|
1720
|
+
isError: false,
|
|
1721
|
+
}
|
|
1722
|
+
})
|
|
1723
|
+
const settleRest = (): void => {
|
|
1724
|
+
for (const id of handedBack) {
|
|
1725
|
+
if (reported.has(id)) continue
|
|
1726
|
+
const name = toolCalls.find((call) => call.id === id)?.function.name ?? 'tool'
|
|
1727
|
+
reportLate({
|
|
1728
|
+
toolCallId: id,
|
|
1729
|
+
toolName: name,
|
|
1730
|
+
output: `Error: Tool "${name}" did not complete — the batch failed before it produced a result.`,
|
|
1731
|
+
isError: true,
|
|
1732
|
+
})
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
completion.then(settleRest, settleRest)
|
|
1736
|
+
} else {
|
|
1737
|
+
await completion
|
|
1738
|
+
}
|
|
1739
|
+
} else {
|
|
1740
|
+
await completion
|
|
1741
|
+
}
|
|
1668
1742
|
|
|
1669
1743
|
// Whatever failed above left a hole in `results`; fill every one, so
|
|
1670
1744
|
// the invariant holds by construction rather than by everything having
|
|
@@ -2205,6 +2279,13 @@ export class ToolExecutor {
|
|
|
2205
2279
|
...(this.config.additionalDirectories?.length
|
|
2206
2280
|
? { additionalDirectories: this.config.additionalDirectories }
|
|
2207
2281
|
: {}),
|
|
2282
|
+
...(this.config.authorizationGate
|
|
2283
|
+
? {
|
|
2284
|
+
readDenied: (absolutePath: string) =>
|
|
2285
|
+
this.evaluatePreparedAuthorization('read', { path: absolutePath })?.decision ===
|
|
2286
|
+
'deny',
|
|
2287
|
+
}
|
|
2288
|
+
: {}),
|
|
2208
2289
|
...(this.outputDirectory()
|
|
2209
2290
|
? { retainedOutputDirectory: this.outputDirectory() as string }
|
|
2210
2291
|
: {}),
|
|
@@ -2300,6 +2381,7 @@ export class ToolExecutor {
|
|
|
2300
2381
|
}
|
|
2301
2382
|
: {}),
|
|
2302
2383
|
...(this.config.onJobAwaited ? { onAwaited: this.config.onJobAwaited } : {}),
|
|
2384
|
+
...(this.config.onJobCollected ? { onCollected: this.config.onJobCollected } : {}),
|
|
2303
2385
|
...(this.config.monitorTimeoutCapMs !== undefined
|
|
2304
2386
|
? { monitorTimeoutCapMs: this.config.monitorTimeoutCapMs }
|
|
2305
2387
|
: {}),
|
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
SandboxAcquisitionStopped,
|
|
114
114
|
createDeferredSandbox,
|
|
115
115
|
} from './deferred-sandbox.js'
|
|
116
|
+
import { DetachedCalls } from './detached-calls.js'
|
|
116
117
|
import type { EventTranslator } from './events.js'
|
|
117
118
|
import { finalizeTurn } from './finalize-turn.js'
|
|
118
119
|
import { GuardCoordinator } from './guard.js'
|
|
@@ -150,6 +151,7 @@ import { acquireSandbox } from './sandbox-lifecycle.js'
|
|
|
150
151
|
import {
|
|
151
152
|
SteeringBinding,
|
|
152
153
|
type SteeringChannel,
|
|
154
|
+
formatJobExitNotice,
|
|
153
155
|
formatMonitorEnd,
|
|
154
156
|
formatMonitorEvent,
|
|
155
157
|
isOperatorUserMessage,
|
|
@@ -808,6 +810,28 @@ export interface QueryParams {
|
|
|
808
810
|
*/
|
|
809
811
|
waitForInbound?: (signal: AbortSignal) => Promise<void>
|
|
810
812
|
|
|
813
|
+
/**
|
|
814
|
+
* Answer the operator while a long tool call is still running.
|
|
815
|
+
*
|
|
816
|
+
* Default off. When on, and `waitForOperator` reports the operator wrote while
|
|
817
|
+
* a batch had been running for two seconds, each call not yet finished is answered with a "still running"
|
|
818
|
+
* placeholder so the model can read the message and reply at once. The
|
|
819
|
+
* calls are not interrupted and not repeated: their real results reach the
|
|
820
|
+
* model later in a message that names each call, and the turn is held open
|
|
821
|
+
* for them like any outstanding work. The host's wake is then also consulted
|
|
822
|
+
* during tool batches, not only when the turn would otherwise end. Ignored
|
|
823
|
+
* under per-call review, where the batch is waiting on a person.
|
|
824
|
+
*/
|
|
825
|
+
answerWhileCallsRun?: boolean
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Resolve when the operator wrote, and for no other reason. It is what
|
|
829
|
+
* `answerWhileCallsRun` listens to: a notice from a child agent or mail from
|
|
830
|
+
* a peer also wakes `waitForInbound`, but must not hand a running call back.
|
|
831
|
+
* Abort removes the waiter, as for `waitForInbound`.
|
|
832
|
+
*/
|
|
833
|
+
waitForOperator?: (signal: AbortSignal) => Promise<void>
|
|
834
|
+
|
|
811
835
|
/** Durable host input: normal append, flush and exact acknowledgement before inference. Requires sessionLog. */
|
|
812
836
|
durableInbound?: DurableInboundSource
|
|
813
837
|
|
|
@@ -1484,6 +1508,21 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1484
1508
|
)
|
|
1485
1509
|
: undefined
|
|
1486
1510
|
awaitedJobs?.attach()
|
|
1511
|
+
// A tool result has handed the model this job's exit: the notice that
|
|
1512
|
+
// would announce the same exit is taken back, and the host is told it was
|
|
1513
|
+
// delivered so the session does not wake the model for it either.
|
|
1514
|
+
const collectJob = (id: string): void => {
|
|
1515
|
+
const withdrawn = jobNotices?.withdraw(id) ?? false
|
|
1516
|
+
awaitedJobs?.collected(id)
|
|
1517
|
+
if (!pendingJobNoticeIds.delete(id) && !withdrawn) return
|
|
1518
|
+
try {
|
|
1519
|
+
params.onJobNoticeDelivered?.([id])
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
ctx.log.warn('A background job notice observer failed', {
|
|
1522
|
+
'exception.message': error instanceof Error ? error.message : String(error),
|
|
1523
|
+
})
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1487
1526
|
|
|
1488
1527
|
// Turn-scoped, like `toolGrants` below: what a skill loaded in this turn
|
|
1489
1528
|
// pre-approves ends with the turn. Shared by the executor, where the
|
|
@@ -1513,6 +1552,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1513
1552
|
...(params.backgroundJobs ? { backgroundJobs: params.backgroundJobs } : {}),
|
|
1514
1553
|
...(params.backgroundJobOwner ? { backgroundJobOwner: params.backgroundJobOwner } : {}),
|
|
1515
1554
|
...(awaitedJobs ? { onJobAwaited: (id: string) => awaitedJobs.expect(id) } : {}),
|
|
1555
|
+
...(awaitedJobs ? { onJobCollected: collectJob } : {}),
|
|
1516
1556
|
...(awaitedJobs ? { onMonitorStarted: (id: string) => awaitedJobs.expectMonitor(id) } : {}),
|
|
1517
1557
|
...(params.monitorTimeoutCapMs !== undefined
|
|
1518
1558
|
? { monitorTimeoutCapMs: params.monitorTimeoutCapMs }
|
|
@@ -1611,13 +1651,13 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1611
1651
|
.catch(() => {})
|
|
1612
1652
|
return
|
|
1613
1653
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
)
|
|
1654
|
+
let inlineOutput: ReturnType<NonNullable<typeof params.backgroundJobs>['read']> | undefined
|
|
1655
|
+
try {
|
|
1656
|
+
inlineOutput = params.backgroundJobs?.read(job.id)
|
|
1657
|
+
} catch {
|
|
1658
|
+
// The record is gone; the notice still stands without its output.
|
|
1659
|
+
}
|
|
1660
|
+
jobNotices?.steer(formatJobExitNotice(job, inlineOutput), job.id)
|
|
1621
1661
|
pendingJobNoticeIds.add(job.id)
|
|
1622
1662
|
void eventTranslator
|
|
1623
1663
|
.emitEvent({
|
|
@@ -1821,6 +1861,9 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1821
1861
|
...(monitorNotices ? { monitorNotices } : {}),
|
|
1822
1862
|
...(monitorNotices ? { onMonitorNoticeDelivered: acknowledgeMonitorNotices } : {}),
|
|
1823
1863
|
...(awaitedJobs ? { awaitedJobs } : {}),
|
|
1864
|
+
...(params.answerWhileCallsRun && params.waitForOperator
|
|
1865
|
+
? { detachedCalls: new DetachedCalls(params.waitForOperator) }
|
|
1866
|
+
: {}),
|
|
1824
1867
|
checkpointMgr,
|
|
1825
1868
|
planManager: ctx.planManager,
|
|
1826
1869
|
taskGateway: taskScheduler,
|