@namzu/sdk 44.0.0 → 44.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 +24 -0
- package/dist/agents/ReactiveAgent.d.ts.map +1 -1
- package/dist/agents/ReactiveAgent.js +4 -0
- package/dist/agents/ReactiveAgent.js.map +1 -1
- package/dist/agents/SupervisorAgent.d.ts.map +1 -1
- package/dist/agents/SupervisorAgent.js +5 -0
- package/dist/agents/SupervisorAgent.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +1 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +41 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +1 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/index.d.ts +15 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +1 -0
- package/dist/runtime/query/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 +11 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/tools/task/create.d.ts.map +1 -1
- package/dist/tools/task/create.js +4 -1
- package/dist/tools/task/create.js.map +1 -1
- package/dist/tools/task/list.d.ts.map +1 -1
- package/dist/tools/task/list.js +4 -1
- package/dist/tools/task/list.js.map +1 -1
- package/dist/tools/task/present.d.ts +18 -0
- package/dist/tools/task/present.d.ts.map +1 -0
- package/dist/tools/task/present.js +88 -0
- package/dist/tools/task/present.js.map +1 -0
- package/dist/tools/task/update.d.ts.map +1 -1
- package/dist/tools/task/update.js +9 -1
- package/dist/tools/task/update.js.map +1 -1
- package/dist/turn/reporter.js +1 -1
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/agent/base.d.ts +25 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +14 -0
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +8 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/utils/log/redact.js +1 -1
- package/dist/utils/log/redact.js.map +1 -1
- package/dist/utils/log/sinks.d.ts +1 -1
- package/dist/utils/log/sinks.js +2 -2
- package/dist/utils/log/sinks.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/ReactiveAgent.ts +4 -0
- package/src/agents/SupervisorAgent.ts +5 -0
- package/src/bridge/sse/mapper.ts +1 -0
- package/src/manager/agent/lifecycle.ts +46 -0
- package/src/runtime/query/events.ts +1 -0
- package/src/runtime/query/index.ts +17 -0
- package/src/runtime/query/iteration/phases/context.ts +2 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +12 -1
- package/src/tools/task/create.ts +4 -1
- package/src/tools/task/list.ts +4 -1
- package/src/tools/task/present.ts +102 -0
- package/src/tools/task/update.ts +9 -1
- package/src/turn/reporter.ts +1 -1
- package/src/types/agent/base.ts +26 -0
- package/src/types/agent/task.ts +15 -0
- package/src/types/session/events.ts +8 -0
- package/src/utils/log/redact.ts +1 -1
- package/src/utils/log/sinks.ts +2 -2
|
@@ -180,6 +180,26 @@ function mergeEnv(
|
|
|
180
180
|
return { ...base, ...override }
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* One `reviewAllowedCalls` that answers `true` when any of `sources` does.
|
|
185
|
+
*
|
|
186
|
+
* Review is only ever added along a delegation: a child's own answer joins
|
|
187
|
+
* its parent's, it never replaces it. Each source is read at every call, so
|
|
188
|
+
* a parent whose answer changes mid-turn (a mode the operator entered) is
|
|
189
|
+
* seen by a child already running. The same function twice (a builder that
|
|
190
|
+
* forwarded the override it was handed) is consulted once. Returns
|
|
191
|
+
* `undefined` when no source is set, and the one source unwrapped when there
|
|
192
|
+
* is exactly one.
|
|
193
|
+
*/
|
|
194
|
+
function anyReviewAllowedCalls(
|
|
195
|
+
...sources: ReadonlyArray<(() => boolean) | undefined>
|
|
196
|
+
): (() => boolean) | undefined {
|
|
197
|
+
const present = [...new Set(sources.filter((s): s is () => boolean => s !== undefined))]
|
|
198
|
+
if (present.length === 0) return undefined
|
|
199
|
+
if (present.length === 1) return present[0]
|
|
200
|
+
return () => present.some((source) => source() === true)
|
|
201
|
+
}
|
|
202
|
+
|
|
183
203
|
export class AgentManager {
|
|
184
204
|
private registry: AgentRegistry
|
|
185
205
|
private instances: Map<TaskId, AgentTask> = new Map()
|
|
@@ -542,6 +562,16 @@ export class AgentManager {
|
|
|
542
562
|
const ownDenies = options.toolScope?.deny ?? []
|
|
543
563
|
const resolvedDenies = [...new Set([...inheritedDenies, ...ownDenies])]
|
|
544
564
|
|
|
565
|
+
// The parent's "review even what the rules allow" (plan mode), and
|
|
566
|
+
// this spawn's own if it names one. OR-ed rather than replaced, for
|
|
567
|
+
// the same reason the denies above are a union: a descendant may ask
|
|
568
|
+
// for more review and never for less. The builder's value joins
|
|
569
|
+
// below, once the builder has run.
|
|
570
|
+
const inheritedReview = anyReviewAllowedCalls(
|
|
571
|
+
context.reviewAllowedCalls,
|
|
572
|
+
options.configOverrides?.reviewAllowedCalls,
|
|
573
|
+
)
|
|
574
|
+
|
|
545
575
|
const childContext: AgentTaskContext = {
|
|
546
576
|
parentSessionId: context.parentSessionId,
|
|
547
577
|
parentTurnId: context.parentTurnId,
|
|
@@ -557,6 +587,7 @@ export class AgentManager {
|
|
|
557
587
|
parentActor: childParentActor,
|
|
558
588
|
...(resolvedDenies.length > 0 ? { toolDenies: resolvedDenies } : {}),
|
|
559
589
|
...(context.childStorage ? { childStorage: context.childStorage } : {}),
|
|
590
|
+
...(inheritedReview ? { reviewAllowedCalls: inheritedReview } : {}),
|
|
560
591
|
}
|
|
561
592
|
|
|
562
593
|
agentTask = Object.assign(queuedTask ?? {}, {
|
|
@@ -759,6 +790,21 @@ export class AgentManager {
|
|
|
759
790
|
childConfig.toolResultGuardrails = inheritedScreens
|
|
760
791
|
}
|
|
761
792
|
|
|
793
|
+
// Stamped after both branches, beside the screens and for the same
|
|
794
|
+
// reason: a `configBuilder` cannot forward a field it was never told
|
|
795
|
+
// about, and the bare-config branch builds its config by hand.
|
|
796
|
+
//
|
|
797
|
+
// Without it a child borrowed its parent's review handler and not
|
|
798
|
+
// the switch that sends rule-allowed and grant-covered batches to
|
|
799
|
+
// it, so plan mode refused the parent's next change and let the
|
|
800
|
+
// child's run: a call an approval earlier in the CHILD's turn
|
|
801
|
+
// covered never reached the handler that knew about plan mode. The
|
|
802
|
+
// function is carried, not sampled, so a mode entered while the
|
|
803
|
+
// child runs reaches its next batch. The builder's own value is
|
|
804
|
+
// kept and OR-ed in: it may add review, never remove the parent's.
|
|
805
|
+
const effectiveReview = anyReviewAllowedCalls(inheritedReview, childConfig.reviewAllowedCalls)
|
|
806
|
+
if (effectiveReview) childConfig.reviewAllowedCalls = effectiveReview
|
|
807
|
+
|
|
762
808
|
// Lineage is assigned by the spawning manager, not proposed by the
|
|
763
809
|
// child definition. A fixed configBuilder can ignore its inputs and
|
|
764
810
|
// configOverrides is caller-authored; neither may turn a child back
|
|
@@ -283,6 +283,7 @@ export class EventTranslator {
|
|
|
283
283
|
status: task.status,
|
|
284
284
|
owner: task.owner,
|
|
285
285
|
...(task.blockedBy.length > 0 ? { blockedBy: task.blockedBy } : {}),
|
|
286
|
+
...(event.type === 'task.deleted' ? { deleted: true as const } : {}),
|
|
286
287
|
})
|
|
287
288
|
break
|
|
288
289
|
default: {
|
|
@@ -739,6 +739,22 @@ export interface QueryParams {
|
|
|
739
739
|
current: import('../../types/permission/index.js').PermissionMode
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
+
/**
|
|
743
|
+
* Whether a batch that needs no review still goes to `resumeHandler`.
|
|
744
|
+
*
|
|
745
|
+
* A batch every call of which an authorization rule allows, or which a
|
|
746
|
+
* grant from earlier in the turn covers, runs without asking the handler.
|
|
747
|
+
* That is right while the handler would approve it anyway, and wrong under
|
|
748
|
+
* a policy stricter than the rules: a read-only mode such as `plan`, which
|
|
749
|
+
* refuses a change a rule would let through. Consulted once per batch,
|
|
750
|
+
* just before either shortcut; `true` sends the batch to the handler, each
|
|
751
|
+
* call carrying the gate's decision in `authorization`. A rule's `deny`
|
|
752
|
+
* still refuses a call whatever this returns.
|
|
753
|
+
*
|
|
754
|
+
* Absent, both shortcuts apply as they always have.
|
|
755
|
+
*/
|
|
756
|
+
reviewAllowedCalls?: () => boolean
|
|
757
|
+
|
|
742
758
|
/**
|
|
743
759
|
* A name for the policy `resumeHandler` implements.
|
|
744
760
|
*
|
|
@@ -1480,6 +1496,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1480
1496
|
// Turn-scoped. An approval is a statement about this turn's work;
|
|
1481
1497
|
// carrying one into a later turn would be reuse nobody agreed to.
|
|
1482
1498
|
toolGrants: new ToolGrantSet(),
|
|
1499
|
+
...(params.reviewAllowedCalls ? { reviewAllowedCalls: params.reviewAllowedCalls } : {}),
|
|
1483
1500
|
// Turn-scoped for the same reason. A repeat count carried into a later
|
|
1484
1501
|
// run is a claim about work nobody repeated, and a module-level map
|
|
1485
1502
|
// would leak exactly that way.
|
|
@@ -166,6 +166,8 @@ export interface IterationContext {
|
|
|
166
166
|
* asked about again. Absent on paths that do not review tools.
|
|
167
167
|
*/
|
|
168
168
|
readonly toolGrants?: ToolGrantSet
|
|
169
|
+
/** See `QueryParams.reviewAllowedCalls`. Absent: allowed and granted batches skip review. */
|
|
170
|
+
readonly reviewAllowedCalls?: () => boolean
|
|
169
171
|
/**
|
|
170
172
|
* Absent when the host opted out with `repeatCallAdvisory: false`. The
|
|
171
173
|
* opt-out is the ABSENCE, not a flag read at every call site, so a code
|
|
@@ -226,6 +226,16 @@ export async function* runToolReview(
|
|
|
226
226
|
// must not be able to release them.
|
|
227
227
|
const gateDenied = new Map<string, string>()
|
|
228
228
|
|
|
229
|
+
// Sampled once, the first time a shortcut asks, so both shortcuts below
|
|
230
|
+
// read the same answer for this batch. A policy stricter than the rules
|
|
231
|
+
// (a read-only mode) sees the batch rather than letting an allowance or a
|
|
232
|
+
// grant run it past the handler.
|
|
233
|
+
let reviewAllowedSample: boolean | undefined
|
|
234
|
+
const reviewAllowed = (): boolean => {
|
|
235
|
+
reviewAllowedSample ??= ctx.reviewAllowedCalls?.() === true
|
|
236
|
+
return reviewAllowedSample
|
|
237
|
+
}
|
|
238
|
+
|
|
229
239
|
// The operator's policy runs FIRST, and a grant cannot overrule it.
|
|
230
240
|
//
|
|
231
241
|
// The grant short-circuit used to sit above this block and return, so a
|
|
@@ -289,7 +299,7 @@ export async function* runToolReview(
|
|
|
289
299
|
const allAllowed = gateResults.every((gr) => gr.gateResult.decision === 'allow')
|
|
290
300
|
const allDenied = gateResults.every((gr) => gr.gateResult.decision === 'deny')
|
|
291
301
|
|
|
292
|
-
if (allAllowed) {
|
|
302
|
+
if (allAllowed && !reviewAllowed()) {
|
|
293
303
|
ctx.log.debug('Authorization gate: all tool calls pre-approved', {
|
|
294
304
|
'namzu.tool.names': gateResults.map((gr) => gr.toolCall.name),
|
|
295
305
|
})
|
|
@@ -334,6 +344,7 @@ export async function* runToolReview(
|
|
|
334
344
|
// working directory or a run outside the sandbox.
|
|
335
345
|
if (
|
|
336
346
|
ctx.toolGrants &&
|
|
347
|
+
!reviewAllowed() &&
|
|
337
348
|
gateDenied.size === 0 &&
|
|
338
349
|
escalated().length === 0 &&
|
|
339
350
|
toolCallSummaries.every((tc) => ctx.toolGrants?.covers(tc))
|
package/src/tools/task/create.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { ToolDefinition } from '../../types/tool/index.js'
|
|
|
4
4
|
import { asTaskId } from '../../utils/id.js'
|
|
5
5
|
import { defineTool } from '../defineTool.js'
|
|
6
6
|
import type { TaskToolScope } from './index.js'
|
|
7
|
+
import { presentTaskCreateCall, presentTaskCreateResult } from './present.js'
|
|
7
8
|
|
|
8
9
|
export function buildTaskCreateTool(
|
|
9
10
|
taskStore: TaskStore,
|
|
@@ -35,6 +36,8 @@ export function buildTaskCreateTool(
|
|
|
35
36
|
readOnly: false,
|
|
36
37
|
destructive: false,
|
|
37
38
|
concurrencySafe: true,
|
|
39
|
+
presentCall: presentTaskCreateCall,
|
|
40
|
+
presentResult: presentTaskCreateResult,
|
|
38
41
|
async execute({ subject, description, activeForm, owner, blockedBy, metadata }) {
|
|
39
42
|
const task = await taskStore.create({
|
|
40
43
|
sessionId: scope.sessionId,
|
|
@@ -50,7 +53,7 @@ export function buildTaskCreateTool(
|
|
|
50
53
|
return {
|
|
51
54
|
success: true,
|
|
52
55
|
output: `Task created: ${task.id} — "${subject}"${owner ? ` [owner: ${owner}]` : ''}`,
|
|
53
|
-
data: { id: task.id, status: task.status, owner: task.owner },
|
|
56
|
+
data: { id: task.id, subject: task.subject, status: task.status, owner: task.owner },
|
|
54
57
|
}
|
|
55
58
|
},
|
|
56
59
|
})
|
package/src/tools/task/list.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type TaskStore, isTerminalTaskStatus } from '../../types/task/index.js'
|
|
|
4
4
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
5
5
|
import { defineTool } from '../defineTool.js'
|
|
6
6
|
import type { TaskToolScope } from './index.js'
|
|
7
|
+
import { countTasks, presentTaskListCall, presentTaskListResult } from './present.js'
|
|
7
8
|
|
|
8
9
|
export function buildTaskListTool(
|
|
9
10
|
taskStore: TaskStore,
|
|
@@ -19,6 +20,8 @@ export function buildTaskListTool(
|
|
|
19
20
|
readOnly: true,
|
|
20
21
|
destructive: false,
|
|
21
22
|
concurrencySafe: true,
|
|
23
|
+
presentCall: presentTaskListCall,
|
|
24
|
+
presentResult: presentTaskListResult,
|
|
22
25
|
async execute() {
|
|
23
26
|
const all = await taskStore.list({ sessionId: scope.sessionId })
|
|
24
27
|
// Blockers resolve against every task of the session, shown or not: a
|
|
@@ -53,7 +56,7 @@ export function buildTaskListTool(
|
|
|
53
56
|
output:
|
|
54
57
|
tasks.length === 0
|
|
55
58
|
? 'No planning tasks yet. This list does not report delegated agent status; use agent_task_list when available.'
|
|
56
|
-
: `${stats.total}
|
|
59
|
+
: `${countTasks(stats.total)}: ${stats.completed} completed, ${stats.in_progress} in progress, ${stats.pending} pending.`,
|
|
57
60
|
data: { tasks: summary, stats },
|
|
58
61
|
}
|
|
59
62
|
},
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { ToolResult } from '../../types/tool/index.js'
|
|
2
|
+
import type { ToolCallView, ToolResultView } from '../../types/tool/presentation.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* How the task tools read to a person.
|
|
6
|
+
*
|
|
7
|
+
* The model-facing output of these tools names task ids, because the model
|
|
8
|
+
* needs them to call `task_update`. None of that is for the operator: with no
|
|
9
|
+
* opinion of its own a task call fell through to the generic presenter, which
|
|
10
|
+
* printed the arguments (`{"id":"01a0…","status":"completed"}`) as the call
|
|
11
|
+
* row and the model's receipt (`Task 01a0… updated — status: completed`) as
|
|
12
|
+
* the result. So each tool says what happened in words — `Added task ·
|
|
13
|
+
* Write the parser` — and a successful receipt adds nothing beneath it.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately no ids, no owner and no JSON anywhere in these views. A host
|
|
16
|
+
* that wants the ids still has the tool result and its `data`.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const MAX_SUBJECT = 100
|
|
20
|
+
|
|
21
|
+
function oneLine(value: string): string {
|
|
22
|
+
const flat = value.replace(/\s+/g, ' ').trim()
|
|
23
|
+
return flat.length > MAX_SUBJECT ? `${flat.slice(0, MAX_SUBJECT - 1)}…` : flat
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function activity(label: string): ToolCallView {
|
|
27
|
+
return { kind: 'generic', presentation: 'activity', label }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A successful receipt: the call row already said it. */
|
|
31
|
+
const HIDDEN_RESULT: ToolResultView = { kind: 'generic', label: '', visibility: 'hidden' }
|
|
32
|
+
|
|
33
|
+
/** `N task` / `N tasks`. */
|
|
34
|
+
export function countTasks(count: number): string {
|
|
35
|
+
return `${count} task${count === 1 ? '' : 's'}`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** What a failed task call says, without the id the model passed. */
|
|
39
|
+
function failure(result: ToolResult, fallback: string): ToolResultView {
|
|
40
|
+
const text = `${result.error ?? result.output ?? ''}`
|
|
41
|
+
return {
|
|
42
|
+
kind: 'generic',
|
|
43
|
+
label: /not found/i.test(text) ? 'No task has that id' : fallback,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function presentTaskCreateCall(input: { readonly subject?: unknown }): ToolCallView {
|
|
48
|
+
const subject = typeof input.subject === 'string' ? oneLine(input.subject) : ''
|
|
49
|
+
return activity(subject ? `Add task · ${subject}` : 'Add task')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function presentTaskCreateResult(_input: unknown, result: ToolResult): ToolResultView {
|
|
53
|
+
if (!result.success) return failure(result, 'The task was not added')
|
|
54
|
+
return HIDDEN_RESULT
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The verb for a `task_update` call, from the status it asks for. */
|
|
58
|
+
export function taskUpdateVerb(status: unknown): string {
|
|
59
|
+
switch (status) {
|
|
60
|
+
case 'in_progress':
|
|
61
|
+
return 'Start task'
|
|
62
|
+
case 'completed':
|
|
63
|
+
return 'Complete task'
|
|
64
|
+
case 'deleted':
|
|
65
|
+
return 'Remove task'
|
|
66
|
+
case 'pending':
|
|
67
|
+
return 'Reopen task'
|
|
68
|
+
default:
|
|
69
|
+
return 'Update task'
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function presentTaskUpdateCall(input: {
|
|
74
|
+
readonly status?: unknown
|
|
75
|
+
readonly subject?: unknown
|
|
76
|
+
}): ToolCallView {
|
|
77
|
+
const verb = taskUpdateVerb(input.status)
|
|
78
|
+
const subject = typeof input.subject === 'string' ? oneLine(input.subject) : ''
|
|
79
|
+
return activity(subject ? `${verb} · ${subject}` : verb)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function presentTaskUpdateResult(_input: unknown, result: ToolResult): ToolResultView {
|
|
83
|
+
if (!result.success) return failure(result, 'The task was not changed')
|
|
84
|
+
return HIDDEN_RESULT
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function presentTaskListCall(): ToolCallView {
|
|
88
|
+
return activity('Check tasks')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function presentTaskListResult(_input: unknown, result: ToolResult): ToolResultView {
|
|
92
|
+
if (!result.success) return { kind: 'generic', label: 'The task list could not be read' }
|
|
93
|
+
const stats = (result.data as { stats?: { total?: unknown; completed?: unknown } } | undefined)
|
|
94
|
+
?.stats
|
|
95
|
+
const total = typeof stats?.total === 'number' ? stats.total : undefined
|
|
96
|
+
const completed = typeof stats?.completed === 'number' ? stats.completed : undefined
|
|
97
|
+
if (total === undefined || completed === undefined) return HIDDEN_RESULT
|
|
98
|
+
return {
|
|
99
|
+
kind: 'generic',
|
|
100
|
+
label: total === 0 ? 'No tasks yet' : `Tasks · ${completed}/${total} done`,
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/tools/task/update.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { TaskStore } from '../../types/task/index.js'
|
|
|
3
3
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
4
4
|
import { asTaskId } from '../../utils/id.js'
|
|
5
5
|
import { defineTool } from '../defineTool.js'
|
|
6
|
+
import { presentTaskUpdateCall, presentTaskUpdateResult } from './present.js'
|
|
6
7
|
|
|
7
8
|
export function buildTaskUpdateTool(taskStore: TaskStore): ToolDefinition {
|
|
8
9
|
return defineTool({
|
|
@@ -34,6 +35,8 @@ export function buildTaskUpdateTool(taskStore: TaskStore): ToolDefinition {
|
|
|
34
35
|
readOnly: false,
|
|
35
36
|
destructive: false,
|
|
36
37
|
concurrencySafe: true,
|
|
38
|
+
presentCall: presentTaskUpdateCall,
|
|
39
|
+
presentResult: presentTaskUpdateResult,
|
|
37
40
|
async execute({
|
|
38
41
|
id,
|
|
39
42
|
subject,
|
|
@@ -85,7 +88,12 @@ export function buildTaskUpdateTool(taskStore: TaskStore): ToolDefinition {
|
|
|
85
88
|
return {
|
|
86
89
|
success: true,
|
|
87
90
|
output: `Task ${id} updated — status: ${updated.status}`,
|
|
88
|
-
data: {
|
|
91
|
+
data: {
|
|
92
|
+
id: updated.id,
|
|
93
|
+
subject: updated.subject,
|
|
94
|
+
status: updated.status,
|
|
95
|
+
owner: updated.owner,
|
|
96
|
+
},
|
|
89
97
|
}
|
|
90
98
|
},
|
|
91
99
|
})
|
package/src/turn/reporter.ts
CHANGED
|
@@ -170,7 +170,7 @@ export function createTurnReporter(parentLogger?: Logger): TurnReporter {
|
|
|
170
170
|
break
|
|
171
171
|
|
|
172
172
|
case 'task_updated':
|
|
173
|
-
log.info('Task updated', {
|
|
173
|
+
log.info(event.deleted ? 'Task removed' : 'Task updated', {
|
|
174
174
|
'namzu.task.subject': event.subject,
|
|
175
175
|
[NAMZU.TURN_ID]: event.turnId,
|
|
176
176
|
'namzu.task.id': event.taskId,
|
package/src/types/agent/base.ts
CHANGED
|
@@ -293,6 +293,32 @@ export interface BaseAgentConfig {
|
|
|
293
293
|
* host that never wired one is unaffected.
|
|
294
294
|
*/
|
|
295
295
|
resumeHandler?: ResumeHandler
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Whether a batch that needs no review still goes to `resumeHandler`, in
|
|
299
|
+
* this agent's turn and in every turn it delegates to. See
|
|
300
|
+
* {@link import('../../runtime/query/index.js').QueryParams.reviewAllowedCalls}.
|
|
301
|
+
*
|
|
302
|
+
* A delegated child borrows its parent's handler, and a handler that
|
|
303
|
+
* refuses a change in a read-only mode (`plan`) cannot refuse a batch that
|
|
304
|
+
* never reaches it: one a rule allows, or one an approval given earlier in
|
|
305
|
+
* the CHILD's turn covers. So `AgentManager` stamps the spawning context's
|
|
306
|
+
* function (`AgentTaskContext.reviewAllowedCalls`) onto the child config
|
|
307
|
+
* after the builder returns, the same way it stamps the handler.
|
|
308
|
+
*
|
|
309
|
+
* Passed as a function and read once per batch, so a mode the operator
|
|
310
|
+
* enters while a child is already running reaches that child's next batch.
|
|
311
|
+
*
|
|
312
|
+
* **It only ever adds review.** A value the child config sets itself (from
|
|
313
|
+
* its `configBuilder` or `configOverrides`) is kept, and consulted together
|
|
314
|
+
* with the inherited one: the child's batch goes to review when EITHER
|
|
315
|
+
* says so. A child can ask for more review than its parent; it cannot
|
|
316
|
+
* answer `false` over a parent that answers `true`.
|
|
317
|
+
*
|
|
318
|
+
* Absent, and nothing inherited: rule-allowed and grant-covered batches run
|
|
319
|
+
* without asking, as they always have.
|
|
320
|
+
*/
|
|
321
|
+
reviewAllowedCalls?: () => boolean
|
|
296
322
|
}
|
|
297
323
|
|
|
298
324
|
export type RuntimeToolOverrides = Record<string, ToolAvailability | 'disabled'>
|
package/src/types/agent/task.ts
CHANGED
|
@@ -142,6 +142,21 @@ export interface AgentTaskContext {
|
|
|
142
142
|
*/
|
|
143
143
|
readonly childStorage?: ChildSessionStorage
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The parent's `reviewAllowedCalls`, handed to every child it delegates
|
|
147
|
+
* to and on to theirs. See `BaseAgentConfig.reviewAllowedCalls`.
|
|
148
|
+
*
|
|
149
|
+
* The function itself, not a sampled value, so a stricter mode entered
|
|
150
|
+
* while a child runs reaches that child's next batch. `AgentManager`
|
|
151
|
+
* stamps it onto the child config after the builder runs and carries it
|
|
152
|
+
* on the child's own context; a child config that sets its own keeps it,
|
|
153
|
+
* and the two are OR-ed, so a descendant can add review and never shed
|
|
154
|
+
* it. `SupervisorAgent` sets it from its config; a host that builds its
|
|
155
|
+
* own context for a `TaskScheduler` sets it from the same function it
|
|
156
|
+
* passes its own `query()`. Absent: children decide from their own config.
|
|
157
|
+
*/
|
|
158
|
+
readonly reviewAllowedCalls?: () => boolean
|
|
159
|
+
|
|
145
160
|
/** Isolation boundary. Required per session-hierarchy.md §12.1. */
|
|
146
161
|
tenantId: TenantId
|
|
147
162
|
|
|
@@ -944,6 +944,14 @@ type CoreSessionEvent =
|
|
|
944
944
|
owner?: string
|
|
945
945
|
/** See `task_created`. Carried on updates because an edge can be added later. */
|
|
946
946
|
blockedBy?: readonly TaskId[]
|
|
947
|
+
/**
|
|
948
|
+
* The task was removed from the list (`task_update` with status
|
|
949
|
+
* `deleted`). `status` and `subject` are what it had when it went.
|
|
950
|
+
* Absent on every other update: without it a removal reached a
|
|
951
|
+
* reader as an update that changed nothing, so a checklist kept
|
|
952
|
+
* drawing the task as open.
|
|
953
|
+
*/
|
|
954
|
+
deleted?: true
|
|
947
955
|
}
|
|
948
956
|
| {
|
|
949
957
|
type: 'plugin_hook_executing'
|
package/src/utils/log/redact.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Runs once, in the pipeline, ahead of sink dispatch — not inside
|
|
4
4
|
// jsonLinesSink, and not inside any other individual sink. A scan that only
|
|
5
|
-
// jsonLinesSink ran would leave prettySink (the one `namzu
|
|
5
|
+
// jsonLinesSink ran would leave prettySink (the one `namzu exec` actually
|
|
6
6
|
// uses) and every host-supplied sink with no second layer at all, which is
|
|
7
7
|
// the gap a scoped-to-one-sink design would reintroduce: the machine-read
|
|
8
8
|
// path would be the only protected one.
|
package/src/utils/log/sinks.ts
CHANGED
|
@@ -19,7 +19,7 @@ export const NOOP_SINK: LogSink = {
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* One JSON object per line — the canonical wire format for the machine-read
|
|
22
|
-
* path (`namzu
|
|
22
|
+
* path (`namzu exec --json`'s stderr). JSON string-escaping neutralises
|
|
23
23
|
* `\n`/`\r` by construction, closing the log-forging surface without a
|
|
24
24
|
* single character-stripping call site, which is bypassable anyway.
|
|
25
25
|
*
|
|
@@ -67,7 +67,7 @@ const SEVERITY_LABEL: Record<LogRecord['severityText'], string> = {
|
|
|
67
67
|
// guarantee yet that either is a constant (that lands with the CI gate in
|
|
68
68
|
// later work) — a remote MCP server that names itself an escape sequence
|
|
69
69
|
// erasing the previous line and printing a fake refusal is a real forging
|
|
70
|
-
// attempt against the terminal `namzu
|
|
70
|
+
// attempt against the terminal `namzu exec` writes to today, not a
|
|
71
71
|
// hypothetical one.
|
|
72
72
|
// biome-ignore lint/suspicious/noControlCharactersInRegex: this pattern IS the control-character filter — the escapes below are ASCII text (`\x00`-`\x1F`, `\x7F`), not raw bytes pasted into the source.
|
|
73
73
|
const CONTROL_BYTE = /[\x00-\x1F\x7F]/g
|