@namzu/sdk 44.0.0 → 44.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- 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/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/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/bridge/sse/mapper.ts +1 -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/session/events.ts +8 -0
- package/src/utils/log/redact.ts +1 -1
- package/src/utils/log/sinks.ts +2 -2
package/src/bridge/sse/mapper.ts
CHANGED
|
@@ -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,
|
|
@@ -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
|