@namzu/sdk 52.1.0 → 52.3.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 +16 -0
- package/README.md +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +3 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +18 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/connector/tools/adapter.d.ts +2 -2
- package/dist/contracts/a2a.d.ts +92 -92
- package/dist/contracts/session/api.d.ts +1 -1
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/manager/resident/learning.d.ts +6 -6
- package/dist/manager/resident/store.d.ts +4 -4
- package/dist/pals/activity/subscription-schema.d.ts +48 -48
- package/dist/pals/channels/routing.d.ts +1 -1
- package/dist/pals/channels/schema.d.ts +7 -7
- package/dist/pals/communication/ingress-schema.d.ts +224 -224
- package/dist/pals/communication/schema.d.ts +18 -18
- package/dist/peers/protocol.d.ts +36 -36
- package/dist/public-runtime.d.ts +2 -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/public-tools.d.ts +3 -1
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +7 -1
- package/dist/public-tools.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +59 -2
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.js +108 -5
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
- package/dist/runtime/jobs/monitor-stream.d.ts +96 -0
- package/dist/runtime/jobs/monitor-stream.d.ts.map +1 -0
- package/dist/runtime/jobs/monitor-stream.js +221 -0
- package/dist/runtime/jobs/monitor-stream.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +96 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +203 -20
- package/dist/runtime/jobs/registry.js.map +1 -1
- 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 +7 -0
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +18 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +94 -3
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts +14 -0
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +64 -3
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +9 -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 +11 -2
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/release-turn.d.ts.map +1 -1
- package/dist/runtime/query/release-turn.js +35 -0
- package/dist/runtime/query/release-turn.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +35 -0
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +121 -0
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +5 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +5 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/runtime/system-events.d.ts +9 -0
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +12 -0
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +3 -0
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +3 -2
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/browser.d.ts +4 -4
- package/dist/tools/builtins/code-analysis.d.ts +49 -0
- package/dist/tools/builtins/code-analysis.d.ts.map +1 -0
- package/dist/tools/builtins/code-analysis.js +409 -0
- package/dist/tools/builtins/code-analysis.js.map +1 -0
- package/dist/tools/builtins/computer-use.d.ts +24 -24
- package/dist/tools/builtins/index.d.ts +2 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +2 -0
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +6 -2
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/lsp.d.ts +1 -1
- package/dist/tools/builtins/monitor.d.ts +24 -0
- package/dist/tools/builtins/monitor.d.ts.map +1 -0
- package/dist/tools/builtins/monitor.js +167 -0
- package/dist/tools/builtins/monitor.js.map +1 -0
- package/dist/tools/builtins/skill.d.ts +2 -2
- package/dist/tools/builtins/wait-for-job.d.ts +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +8 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/code-analysis/index.d.ts +299 -0
- package/dist/types/code-analysis/index.d.ts.map +1 -0
- package/dist/types/code-analysis/index.js +30 -0
- package/dist/types/code-analysis/index.js.map +1 -0
- package/dist/types/plugin/index.d.ts +6 -6
- package/dist/types/session/events.d.ts +30 -4
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js +6 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +129 -129
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +3 -2
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +34 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/a2a/mapper.ts +3 -0
- package/src/bridge/sse/mapper.ts +18 -0
- package/src/contracts/session/api.ts +1 -0
- package/src/index.ts +23 -0
- package/src/public-runtime.ts +28 -0
- package/src/public-tools.ts +16 -0
- package/src/runtime/jobs/awaited-jobs.ts +127 -5
- package/src/runtime/jobs/monitor-stream.ts +269 -0
- package/src/runtime/jobs/registry.ts +286 -21
- package/src/runtime/query/executor.ts +17 -0
- package/src/runtime/query/index.ts +119 -3
- package/src/runtime/query/iteration/outstanding-work.ts +64 -3
- package/src/runtime/query/iteration/phases/context.ts +9 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +21 -9
- package/src/runtime/query/release-turn.ts +33 -0
- package/src/runtime/query/steering.ts +144 -0
- package/src/runtime/query/tooling.ts +10 -0
- package/src/runtime/system-events.ts +17 -0
- package/src/tools/builtins/bash.ts +3 -2
- package/src/tools/builtins/code-analysis.ts +486 -0
- package/src/tools/builtins/index.ts +11 -0
- package/src/tools/builtins/job.ts +11 -2
- package/src/tools/builtins/monitor.ts +206 -0
- package/src/turn/reporter.ts +8 -0
- package/src/types/code-analysis/index.ts +325 -0
- package/src/types/session/events.ts +31 -4
- package/src/types/session/records.ts +3 -2
- package/src/types/tool/index.ts +33 -1
|
@@ -5,7 +5,7 @@ import type { TaskHandle } from '../../../types/agent/scheduler.js'
|
|
|
5
5
|
import { createRuntimeContextMessage } from '../../../types/message/index.js'
|
|
6
6
|
import type { SessionEvent } from '../../../types/session/index.js'
|
|
7
7
|
import { readPositiveIntEnv } from '../../../utils/env.js'
|
|
8
|
-
import { formatJobNote } from '../steering.js'
|
|
8
|
+
import { formatJobNote, formatMonitorNote } from '../steering.js'
|
|
9
9
|
import type { IterationContext } from './phases/index.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -179,7 +179,17 @@ export async function* holdForOutstandingWork(
|
|
|
179
179
|
// to be running. A job therefore never shortens a wait, and it never
|
|
180
180
|
// lengthens one either: where a task is outstanding too, that is how
|
|
181
181
|
// long this turn was waiting anyway.
|
|
182
|
-
|
|
182
|
+
let graceMs = inbox ? settleGraceMs(remainingMs) : awaitedJobGraceMs(remainingMs)
|
|
183
|
+
// A monitor the turn owns is held for until the monitor itself ends, which
|
|
184
|
+
// its own deadline bounds; the two-minute job ceiling would walk away from
|
|
185
|
+
// a watch with most of its time left. As with a task, this only ever
|
|
186
|
+
// lengthens the wait: it is the longest justified leg that sets the
|
|
187
|
+
// deadline. A second of slack lets the expiry notice arrive before the
|
|
188
|
+
// grace does.
|
|
189
|
+
const monitorMs = jobs?.monitorHoldMs()
|
|
190
|
+
if (monitorMs !== undefined) {
|
|
191
|
+
graceMs = Math.max(graceMs, Math.min(settleGraceMs(remainingMs), monitorMs + 1000))
|
|
192
|
+
}
|
|
183
193
|
ctx.log.info('Holding the turn open for outstanding work', {
|
|
184
194
|
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
185
195
|
[NAMZU.ITERATION]: iterationNum,
|
|
@@ -217,8 +227,9 @@ export async function* holdForOutstandingWork(
|
|
|
217
227
|
)
|
|
218
228
|
}
|
|
219
229
|
const exited = deliverAwaitedJobExits(ctx)
|
|
230
|
+
const monitored = deliverMonitorNotices(ctx)
|
|
220
231
|
const inbound = await deliverInbound()
|
|
221
|
-
if (arrived.length === 0 && !exited && inbound === 0) return false
|
|
232
|
+
if (arrived.length === 0 && !exited && !monitored && inbound === 0) return false
|
|
222
233
|
await ctx.emitEvent({
|
|
223
234
|
type: 'iteration_completed',
|
|
224
235
|
turnId: ctx.recorder.turnId,
|
|
@@ -268,6 +279,32 @@ export function deliverAwaitedJobExits(ctx: IterationContext): boolean {
|
|
|
268
279
|
return true
|
|
269
280
|
}
|
|
270
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Put what a monitor the turn owns has said in front of the model.
|
|
284
|
+
*
|
|
285
|
+
* The same shape as {@link deliverAwaitedJobExits}: through the channel the
|
|
286
|
+
* events already travel on (`monitorNotices`, which `attachNotice` rides out on
|
|
287
|
+
* the next tool result), and as a `runtime-context` message when the turn made
|
|
288
|
+
* no tool call and so has no result to carry it. The record that they were
|
|
289
|
+
* delivered goes with the text, so an event already attached to a tool result
|
|
290
|
+
* earlier in the turn leaves nothing here and does not buy another turn.
|
|
291
|
+
*
|
|
292
|
+
* The message kind is `job-exit`: a monitor is a job, and no host reads a
|
|
293
|
+
* separate kind for it. What it is carrying (events, or the end) is in the text.
|
|
294
|
+
*/
|
|
295
|
+
export function deliverMonitorNotices(ctx: IterationContext): boolean {
|
|
296
|
+
if (!ctx.monitorNotices) return false
|
|
297
|
+
const text = ctx.awaitedJobs?.takeMonitorDelivery(() => ctx.monitorNotices?.drain())
|
|
298
|
+
if (text === undefined) return false
|
|
299
|
+
|
|
300
|
+
ctx.log.info('Delivering monitor events the turn held itself open for', {
|
|
301
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
302
|
+
})
|
|
303
|
+
ctx.recorder.pushMessage(createRuntimeContextMessage(formatMonitorNote(text), 'job-exit'))
|
|
304
|
+
ctx.onMonitorNoticeDelivered?.()
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
|
|
271
308
|
/** Await arrived tracking writes within the caller's existing turn deadline. */
|
|
272
309
|
export async function drainArrivedTaskCompletions(ctx: IterationContext): Promise<TaskHandle[]> {
|
|
273
310
|
const inbox = ctx.completionInbox
|
|
@@ -418,6 +455,7 @@ export function deliverArrivedCompletions(ctx: IterationContext): void {
|
|
|
418
455
|
* messages land in the order the work finished in.
|
|
419
456
|
*/
|
|
420
457
|
export function deliverArrivedJobExits(ctx: IterationContext): void {
|
|
458
|
+
deliverArrivedMonitorNotices(ctx)
|
|
421
459
|
const delivered = ctx.awaitedJobs?.takeDelivery(() => ctx.jobNotices?.drain())
|
|
422
460
|
if (!delivered) return
|
|
423
461
|
|
|
@@ -434,3 +472,26 @@ export function deliverArrivedJobExits(ctx: IterationContext): void {
|
|
|
434
472
|
ctx.recorder.pushMessage(createRuntimeContextMessage(formatJobNote(delivered.text), 'job-exit'))
|
|
435
473
|
ctx.onJobNoticeDelivered?.()
|
|
436
474
|
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* The monitor half of {@link deliverArrivedJobExits}: what a held monitor said
|
|
478
|
+
* after the hold stopped looking is still delivered on the way out, so the
|
|
479
|
+
* transcript has it and a continued thread opens with it. Only for monitors
|
|
480
|
+
* the turn holds itself open for; a session's host hears its own.
|
|
481
|
+
*/
|
|
482
|
+
function deliverArrivedMonitorNotices(ctx: IterationContext): void {
|
|
483
|
+
if (!ctx.monitorNotices) return
|
|
484
|
+
const text = ctx.awaitedJobs?.takeMonitorDelivery(() => ctx.monitorNotices?.drain())
|
|
485
|
+
if (text === undefined) return
|
|
486
|
+
|
|
487
|
+
// Fix the turn's answer BEFORE appending anything after it; see
|
|
488
|
+
// `deliverArrivedCompletions` for why the tail walk needs it.
|
|
489
|
+
const answer = ctx.recorder.materializeResult()
|
|
490
|
+
if (answer.length > 0) ctx.recorder.setResult(answer, 'outstanding_work')
|
|
491
|
+
|
|
492
|
+
ctx.log.info('Delivering monitor events the turn would have settled over', {
|
|
493
|
+
[NAMZU.TURN_ID]: ctx.recorder.turnId,
|
|
494
|
+
})
|
|
495
|
+
ctx.recorder.pushMessage(createRuntimeContextMessage(formatMonitorNote(text), 'job-exit'))
|
|
496
|
+
ctx.onMonitorNoticeDelivered?.()
|
|
497
|
+
}
|
|
@@ -148,6 +148,15 @@ export interface IterationContext {
|
|
|
148
148
|
readonly jobNotices?: SteeringChannel
|
|
149
149
|
/** Acknowledge exit notices only after their text enters model context. */
|
|
150
150
|
readonly onJobNoticeDelivered?: () => void
|
|
151
|
+
/**
|
|
152
|
+
* What the turn's monitors have said (and how they ended), drained into the
|
|
153
|
+
* next tool result like {@link jobNotices}. A separate channel because its
|
|
154
|
+
* text is the system-event envelope of untrusted process output, and
|
|
155
|
+
* because a host acknowledges it separately.
|
|
156
|
+
*/
|
|
157
|
+
readonly monitorNotices?: SteeringChannel
|
|
158
|
+
/** Acknowledge monitor notices only after their text enters model context. */
|
|
159
|
+
readonly onMonitorNoticeDelivered?: () => void
|
|
151
160
|
/**
|
|
152
161
|
* Background jobs the model said it is waiting on, which is the only kind
|
|
153
162
|
* the loop holds a finishing run open for.
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from '../../project-instructions.js'
|
|
14
14
|
import { attachRepeatNotice } from '../../repeat-call.js'
|
|
15
15
|
import { isReviewExempt } from '../../review-policy.js'
|
|
16
|
-
import { attachNotice, attachSteering, formatJobNote } from '../../steering.js'
|
|
16
|
+
import { attachNotice, attachSteering, formatJobNote, formatMonitorNote } from '../../steering.js'
|
|
17
17
|
import { type IterationContext, awaitDecisionDurably } from './context.js'
|
|
18
18
|
|
|
19
19
|
interface VerificationAwareContext extends IterationContext {
|
|
@@ -240,17 +240,28 @@ export async function* runToolReview(
|
|
|
240
240
|
// user message wedged between them is rejected by the provider. Same
|
|
241
241
|
// delivery a denial already uses, without the refusal.
|
|
242
242
|
let jobNoticeDelivered = false
|
|
243
|
+
let monitorNoticeDelivered = false
|
|
243
244
|
for (const msg of attachRepeatNotice(
|
|
244
245
|
attachNotice(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
246
|
+
attachNotice(
|
|
247
|
+
attachSteering(batch.messages, ctx.steering, ctx.onSteeringDelivered),
|
|
248
|
+
ctx.jobNotices,
|
|
249
|
+
formatJobNote,
|
|
250
|
+
// The exits that text accounts for have now been read, so the
|
|
251
|
+
// record of them stops being pending work. Left standing, it
|
|
252
|
+
// buys the model a turn the next time any job queues a notice.
|
|
253
|
+
() => {
|
|
254
|
+
ctx.awaitedJobs?.noticesDelivered()
|
|
255
|
+
jobNoticeDelivered = true
|
|
256
|
+
},
|
|
257
|
+
),
|
|
258
|
+
ctx.monitorNotices,
|
|
259
|
+
formatMonitorNote,
|
|
260
|
+
// Same rule for what a monitor said: read here, so a turn holding
|
|
261
|
+
// itself open for the monitor does not spend a turn re-reading it.
|
|
251
262
|
() => {
|
|
252
|
-
ctx.awaitedJobs?.
|
|
253
|
-
|
|
263
|
+
ctx.awaitedJobs?.monitorNoticesDelivered()
|
|
264
|
+
monitorNoticeDelivered = true
|
|
254
265
|
},
|
|
255
266
|
),
|
|
256
267
|
notices,
|
|
@@ -258,6 +269,7 @@ export async function* runToolReview(
|
|
|
258
269
|
ctx.recorder.pushMessage(msg)
|
|
259
270
|
}
|
|
260
271
|
if (jobNoticeDelivered) ctx.onJobNoticeDelivered?.()
|
|
272
|
+
if (monitorNoticeDelivered) ctx.onMonitorNoticeDelivered?.()
|
|
261
273
|
// The complete tool-result batch is already in history before host policy
|
|
262
274
|
// may react, so a replacement cannot split provider-required adjacency.
|
|
263
275
|
// Commit each accepted observation before entering the next one. A single
|
|
@@ -89,8 +89,41 @@ export async function* releaseTurnResources(
|
|
|
89
89
|
// Only jobs bound to this turn. Jobs a host bound to its session are
|
|
90
90
|
// the host's to stop, when the session ends.
|
|
91
91
|
if (backgroundJobs && (backgroundJobOwner ?? ctx.turnId) === ctx.turnId) {
|
|
92
|
+
// A monitor still running here is one the turn held itself open for and
|
|
93
|
+
// stopped waiting on (the hold hit its bound). The exit listener is
|
|
94
|
+
// already unsubscribed, so nobody would tell the model or the host it
|
|
95
|
+
// was stopped: say so here, from the release path itself.
|
|
96
|
+
const monitors = backgroundJobs
|
|
97
|
+
.list(ctx.turnId)
|
|
98
|
+
.filter((job) => job.kind === 'monitor' && job.status === 'running')
|
|
99
|
+
for (const job of monitors) {
|
|
100
|
+
ctx.log.warn('A monitor was still running when its turn ended and is being stopped', {
|
|
101
|
+
[NAMZU.TURN_ID]: ctx.turnId,
|
|
102
|
+
'namzu.jobs.id': job.id,
|
|
103
|
+
'namzu.monitor.description': job.description ?? '',
|
|
104
|
+
})
|
|
105
|
+
}
|
|
92
106
|
try {
|
|
93
107
|
const stopped = await backgroundJobs.killOwner(ctx.turnId)
|
|
108
|
+
for (const job of stopped) {
|
|
109
|
+
if (job.kind !== 'monitor') continue
|
|
110
|
+
await eventTranslator
|
|
111
|
+
.emitEvent({
|
|
112
|
+
type: 'background_job_exited',
|
|
113
|
+
turnId: ctx.turnId,
|
|
114
|
+
jobId: job.id,
|
|
115
|
+
command: job.command,
|
|
116
|
+
status: 'killed',
|
|
117
|
+
...(job.exitCode !== undefined ? { exitCode: job.exitCode } : {}),
|
|
118
|
+
...(job.signal ? { signal: job.signal } : {}),
|
|
119
|
+
monitor: {
|
|
120
|
+
description: job.description ?? '',
|
|
121
|
+
stopReason: job.stopReason ?? 'stopped',
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
.catch(() => {})
|
|
125
|
+
}
|
|
126
|
+
if (monitors.length > 0) yield* eventTranslator.drainPending()
|
|
94
127
|
if (stopped.length > 0) {
|
|
95
128
|
ctx.log.info('Background jobs stopped with the turn', {
|
|
96
129
|
[NAMZU.TURN_ID]: ctx.turnId,
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { Message, UserMessage } from '../../types/message/index.js'
|
|
2
|
+
import {
|
|
3
|
+
MONITOR_EVENT_CONTENT_MAX_CHARS,
|
|
4
|
+
MONITOR_NOTIFICATION_MAX_CHARS,
|
|
5
|
+
} from '../jobs/monitor-stream.js'
|
|
6
|
+
import { type BackgroundJob, type MonitorEvent, describeMonitorEnd } from '../jobs/registry.js'
|
|
7
|
+
import { formatSystemEvent, oneMetadataLine } from '../system-events.js'
|
|
2
8
|
|
|
3
9
|
/** User-role runtime reports are context, not new operator intent. */
|
|
4
10
|
export function isOperatorUserMessage(message: Message): message is UserMessage {
|
|
@@ -135,6 +141,144 @@ export function formatJobNote(text: string): string {
|
|
|
135
141
|
return `\n\n[Background job update]\n${text}`
|
|
136
142
|
}
|
|
137
143
|
|
|
144
|
+
/**
|
|
145
|
+
* What the model reads when a monitor it started has something to say.
|
|
146
|
+
*
|
|
147
|
+
* One notification is bounded by {@link MONITOR_NOTIFICATION_MAX_CHARS}, at the
|
|
148
|
+
* granularity of a whole event: the earliest events say what the watch was and
|
|
149
|
+
* the latest say where it stands, and the middle is the part a reader can best
|
|
150
|
+
* do without. The cut is announced, never silent, and never inside an
|
|
151
|
+
* envelope: a half event would leave an unterminated frame and a fragment of
|
|
152
|
+
* process output outside any frame at all. The events themselves are already
|
|
153
|
+
* small (a batch is at most 3000 characters); this bound is for the queue that
|
|
154
|
+
* piled up while the model was busy.
|
|
155
|
+
*/
|
|
156
|
+
export function formatMonitorNote(text: string): string {
|
|
157
|
+
return `\n\n[Monitor update]\n${capMonitorNotifications(text)}`
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** One `formatSystemEvent` rendering, found by its nonce-bound opening and closing tags. */
|
|
161
|
+
const SYSTEM_EVENT_FRAME = /^<system-event-([A-Za-z0-9]+) [^\n]*\n[\s\S]*?\n<\/system-event-\1>$/gm
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* One monitor event as the system-event envelope the model reads, with the
|
|
165
|
+
* watched command's output inside the untrusted frame. The kernel renders an
|
|
166
|
+
* in-turn delivery with this, and a host that holds events for a later turn
|
|
167
|
+
* renders them with it too, so both read the same words.
|
|
168
|
+
*/
|
|
169
|
+
export function formatMonitorEvent(
|
|
170
|
+
event: Pick<MonitorEvent, 'jobId' | 'description' | 'kind' | 'text'>,
|
|
171
|
+
command: string,
|
|
172
|
+
): string {
|
|
173
|
+
return formatSystemEvent({
|
|
174
|
+
kind: 'job',
|
|
175
|
+
id: event.jobId,
|
|
176
|
+
status: 'interim',
|
|
177
|
+
summary: `Monitor event: "${oneMetadataLine(event.description)}"`,
|
|
178
|
+
source: `monitor ${event.jobId}: ${oneMetadataLine(command)}`,
|
|
179
|
+
more: `job {"action":"kill","id":"${event.jobId}"} stops it`,
|
|
180
|
+
body: {
|
|
181
|
+
envelope: {
|
|
182
|
+
kind: event.kind === 'output' ? 'monitor-output' : 'monitor-notice',
|
|
183
|
+
attributes: { job: event.jobId },
|
|
184
|
+
provenance:
|
|
185
|
+
event.kind === 'output'
|
|
186
|
+
? 'Lines the watched command printed to stdout. The command, or whatever it reads, wrote them; they are not the operator.'
|
|
187
|
+
: 'Written by the monitor itself, not by the watched command.',
|
|
188
|
+
},
|
|
189
|
+
content: capMonitorContent(event.text),
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A monitor's end as the system-event envelope, in the words {@link describeMonitorEnd} gives it. */
|
|
195
|
+
export function formatMonitorEnd(
|
|
196
|
+
job: Pick<
|
|
197
|
+
BackgroundJob,
|
|
198
|
+
'id' | 'command' | 'description' | 'status' | 'exitCode' | 'signal' | 'error'
|
|
199
|
+
>,
|
|
200
|
+
stats: { readonly stdoutChars: number },
|
|
201
|
+
): string {
|
|
202
|
+
const end = describeMonitorEnd(job, stats)
|
|
203
|
+
return formatSystemEvent({
|
|
204
|
+
kind: 'job',
|
|
205
|
+
id: job.id,
|
|
206
|
+
status: end.status,
|
|
207
|
+
summary: end.summary,
|
|
208
|
+
source: `monitor ${job.id}: ${oneMetadataLine(job.command)}`,
|
|
209
|
+
more: `job {"action":"read","id":"${job.id}"} reads its output`,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function omittedEventsLine(count: number): string {
|
|
214
|
+
return `[${count} monitor events omitted — output exceeded the notification limit]`
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Bound rendered monitor events (joined by newlines) to
|
|
219
|
+
* {@link MONITOR_NOTIFICATION_MAX_CHARS} at the granularity of a whole event.
|
|
220
|
+
*/
|
|
221
|
+
export function capMonitorNotifications(
|
|
222
|
+
text: string,
|
|
223
|
+
max: number = MONITOR_NOTIFICATION_MAX_CHARS,
|
|
224
|
+
): string {
|
|
225
|
+
if (text.length <= max) return text
|
|
226
|
+
const frames = [...text.matchAll(SYSTEM_EVENT_FRAME)].map((match) => match[0])
|
|
227
|
+
if (frames.length === 0) return cutMiddle(text, max)
|
|
228
|
+
|
|
229
|
+
const line = (count: number) => omittedEventsLine(count)
|
|
230
|
+
// Newlines between the pieces: head, marker, tail.
|
|
231
|
+
const budget = max - line(frames.length).length - 2
|
|
232
|
+
let head = 0
|
|
233
|
+
let headChars = 0
|
|
234
|
+
while (head < frames.length) {
|
|
235
|
+
const size = (frames[head]?.length ?? 0) + 1
|
|
236
|
+
if (headChars + size > budget / 2) break
|
|
237
|
+
headChars += size
|
|
238
|
+
head++
|
|
239
|
+
}
|
|
240
|
+
let tail = 0
|
|
241
|
+
let tailChars = 0
|
|
242
|
+
while (head + tail < frames.length) {
|
|
243
|
+
const frame = frames[frames.length - 1 - tail]
|
|
244
|
+
const size = (frame?.length ?? 0) + 1
|
|
245
|
+
if (headChars + tailChars + size > budget) break
|
|
246
|
+
tailChars += size
|
|
247
|
+
tail++
|
|
248
|
+
}
|
|
249
|
+
const omitted = frames.length - head - tail
|
|
250
|
+
// A single event alone over the cap is dropped whole rather than cut: its
|
|
251
|
+
// content is shrunk before framing (see `capMonitorContent`), so this only
|
|
252
|
+
// means a caller handed in something that was not.
|
|
253
|
+
const kept = [
|
|
254
|
+
...frames.slice(0, head),
|
|
255
|
+
...(omitted > 0 ? [line(omitted)] : []),
|
|
256
|
+
...frames.slice(frames.length - tail),
|
|
257
|
+
]
|
|
258
|
+
return kept.join('\n')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Shrink one event's content, in the middle, BEFORE it is framed. Cutting the
|
|
263
|
+
* framed text afterwards would cut the envelope.
|
|
264
|
+
*/
|
|
265
|
+
export function capMonitorContent(
|
|
266
|
+
text: string,
|
|
267
|
+
max: number = MONITOR_EVENT_CONTENT_MAX_CHARS,
|
|
268
|
+
): string {
|
|
269
|
+
return text.length <= max ? text : cutMiddle(text, max)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function cutMiddle(text: string, max: number): string {
|
|
273
|
+
if (text.length <= max) return text
|
|
274
|
+
const marker = (omitted: number) => `\n...[${omitted} characters omitted]...\n`
|
|
275
|
+
// Reserve room for the marker itself so the result never exceeds `max`.
|
|
276
|
+
const room = Math.max(0, max - marker(text.length).length)
|
|
277
|
+
const head = Math.ceil(room / 2)
|
|
278
|
+
const tail = room - head
|
|
279
|
+
return `${text.slice(0, head)}${marker(text.length - head - tail)}${text.slice(text.length - tail)}`
|
|
280
|
+
}
|
|
281
|
+
|
|
138
282
|
const STEERING_NOTE_HEADER =
|
|
139
283
|
'\n\n[Message from the operator, received while this tool was running]\n'
|
|
140
284
|
|
|
@@ -19,6 +19,7 @@ import type { EmitEvent } from './events.js'
|
|
|
19
19
|
|
|
20
20
|
export interface ToolingBootstrapConfig {
|
|
21
21
|
fileReadTracker?: import('../../types/tool/index.js').FileReadTracker
|
|
22
|
+
codeAnalysis?: import('../../types/code-analysis/index.js').CodeAnalysisProvider
|
|
22
23
|
tools: ToolManager
|
|
23
24
|
/** Exact runtime output-tool owner for opt-in JSON retention; see ToolExecutorConfig. */
|
|
24
25
|
durableStructuredOutputTool?: ToolDefinition
|
|
@@ -45,6 +46,10 @@ export interface ToolingBootstrapConfig {
|
|
|
45
46
|
backgroundJobOwner?: string
|
|
46
47
|
/** Where `wait_for_job` records wait-intent; see the executor's own field. */
|
|
47
48
|
onJobAwaited?: (id: string) => void
|
|
49
|
+
/** See `ToolExecutorConfig.monitorTimeoutCapMs`. */
|
|
50
|
+
monitorTimeoutCapMs?: number
|
|
51
|
+
/** See `ToolExecutorConfig.onMonitorStarted`. */
|
|
52
|
+
onMonitorStarted?: (id: string) => void
|
|
48
53
|
/** Where the `skill` tool reads from. */
|
|
49
54
|
skills?: SkillRegistryRef
|
|
50
55
|
/** How this turn reaches the web. */
|
|
@@ -86,6 +91,7 @@ export class ToolingBootstrap {
|
|
|
86
91
|
? { durableStructuredOutputTool: config.durableStructuredOutputTool }
|
|
87
92
|
: {}),
|
|
88
93
|
...(config.fileReadTracker ? { fileReadTracker: config.fileReadTracker } : {}),
|
|
94
|
+
...(config.codeAnalysis ? { codeAnalysis: config.codeAnalysis } : {}),
|
|
89
95
|
sessionId: config.sessionId,
|
|
90
96
|
turnId: config.turnId,
|
|
91
97
|
workingDirectory: config.workingDirectory,
|
|
@@ -104,6 +110,10 @@ export class ToolingBootstrap {
|
|
|
104
110
|
...(config.backgroundJobs ? { backgroundJobs: config.backgroundJobs } : {}),
|
|
105
111
|
...(config.backgroundJobOwner ? { backgroundJobOwner: config.backgroundJobOwner } : {}),
|
|
106
112
|
...(config.onJobAwaited ? { onJobAwaited: config.onJobAwaited } : {}),
|
|
113
|
+
...(config.monitorTimeoutCapMs !== undefined
|
|
114
|
+
? { monitorTimeoutCapMs: config.monitorTimeoutCapMs }
|
|
115
|
+
: {}),
|
|
116
|
+
...(config.onMonitorStarted ? { onMonitorStarted: config.onMonitorStarted } : {}),
|
|
107
117
|
...(config.skills ? { skills: config.skills } : {}),
|
|
108
118
|
...(config.web ? { web: config.web } : {}),
|
|
109
119
|
...(config.toolTimeoutMs !== undefined ? { toolTimeoutMs: config.toolTimeoutMs } : {}),
|
|
@@ -148,6 +148,23 @@ function neutralizeSystemEventDelimiter(text: string): string {
|
|
|
148
148
|
return text.replace(SYSTEM_EVENT_KEYWORD, 'system_event')
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/** The longest a model-chosen value may be inside a metadata line. */
|
|
152
|
+
export const SYSTEM_EVENT_METADATA_MAX_CHARS = 200
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* A value the kernel did not author, made safe for ONE metadata line: every
|
|
156
|
+
* whitespace run (newlines included, so it cannot forge a `summary:` or
|
|
157
|
+
* `source:` line of its own) becomes a single space, and what is left is cut
|
|
158
|
+
* to `max` characters with an ellipsis.
|
|
159
|
+
*/
|
|
160
|
+
export function oneMetadataLine(
|
|
161
|
+
value: string,
|
|
162
|
+
max: number = SYSTEM_EVENT_METADATA_MAX_CHARS,
|
|
163
|
+
): string {
|
|
164
|
+
const line = value.replace(/\s+/g, ' ').trim()
|
|
165
|
+
return line.length <= max ? line : `${line.slice(0, Math.max(0, max - 1))}…`
|
|
166
|
+
}
|
|
167
|
+
|
|
151
168
|
/** Escape a value so it cannot rewrite the tag it appears in. See `untrusted-envelope.ts`. */
|
|
152
169
|
function escapeAttribute(value: string): string {
|
|
153
170
|
return value
|
|
@@ -66,7 +66,7 @@ const MAX_BASH_TIMEOUT_MS = readPositiveIntEnv('NAMZU_BASH_MAX_TIMEOUT_MS', 10 *
|
|
|
66
66
|
*/
|
|
67
67
|
export const DISABLE_BACKGROUND_TASKS_ENV = 'NAMZU_DISABLE_BACKGROUND_TASKS'
|
|
68
68
|
|
|
69
|
-
function backgroundTasksDisabled(): boolean {
|
|
69
|
+
export function backgroundTasksDisabled(): boolean {
|
|
70
70
|
const value = process.env[DISABLE_BACKGROUND_TASKS_ENV]?.trim().toLowerCase()
|
|
71
71
|
return value !== undefined && value !== '' && value !== '0' && value !== 'false'
|
|
72
72
|
}
|
|
@@ -442,7 +442,8 @@ export function execHostShell(
|
|
|
442
442
|
})
|
|
443
443
|
}
|
|
444
444
|
|
|
445
|
-
|
|
445
|
+
/** The dangerous-pattern check `bash` applies, shared so the `monitor` tool refuses the same lines. */
|
|
446
|
+
export function isDangerousCommand(command: string): boolean {
|
|
446
447
|
return DANGEROUS_PATTERNS.some((pattern) => pattern.test(command))
|
|
447
448
|
}
|
|
448
449
|
|