@namzu/sdk 61.4.0 → 62.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 +62 -0
- package/dist/agents/QueryAgent.d.ts.map +1 -1
- package/dist/agents/QueryAgent.js +2 -0
- package/dist/agents/QueryAgent.js.map +1 -1
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +2 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/acp/permission.d.ts +8 -0
- package/dist/bridge/acp/permission.d.ts.map +1 -1
- package/dist/bridge/acp/permission.js.map +1 -1
- package/dist/bridge/acp/server.d.ts.map +1 -1
- package/dist/bridge/acp/server.js +8 -1
- package/dist/bridge/acp/server.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +13 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/constants/tools/index.d.ts +2 -0
- package/dist/constants/tools/index.d.ts.map +1 -1
- package/dist/constants/tools/index.js +23 -0
- package/dist/constants/tools/index.js.map +1 -1
- package/dist/contracts/session/api.d.ts +1 -1
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/eval/harness-protection.js +1 -1
- package/dist/eval/harness-protection.js.map +1 -1
- package/dist/learning/cycle.d.ts +5 -3
- package/dist/learning/cycle.d.ts.map +1 -1
- package/dist/learning/cycle.js +11 -13
- package/dist/learning/cycle.js.map +1 -1
- package/dist/learning/guidance.d.ts +74 -0
- package/dist/learning/guidance.d.ts.map +1 -0
- package/dist/learning/guidance.js +259 -0
- package/dist/learning/guidance.js.map +1 -0
- package/dist/learning/skill.d.ts +2 -2
- package/dist/learning/skill.d.ts.map +1 -1
- package/dist/learning/skill.js +6 -6
- package/dist/learning/skill.js.map +1 -1
- package/dist/learning/store.d.ts.map +1 -1
- package/dist/learning/store.js +6 -6
- package/dist/learning/store.js.map +1 -1
- package/dist/pals/communication/ingress-schema.d.ts +8 -8
- package/dist/pals/communication/schema.d.ts +2 -2
- package/dist/peers/client.d.ts +2 -3
- package/dist/peers/client.d.ts.map +1 -1
- package/dist/peers/client.js +2 -3
- package/dist/peers/client.js.map +1 -1
- package/dist/peers/endpoint.d.ts +1 -2
- package/dist/peers/endpoint.d.ts.map +1 -1
- package/dist/peers/endpoint.js +1 -2
- package/dist/peers/endpoint.js.map +1 -1
- package/dist/peers/protocol.d.ts.map +1 -1
- package/dist/peers/protocol.js +1 -0
- package/dist/peers/protocol.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +4 -4
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +5 -3
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/prompt/index.d.ts +0 -4
- package/dist/prompt/index.d.ts.map +1 -1
- package/dist/prompt/index.js +0 -2
- package/dist/prompt/index.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +3 -0
- package/dist/provider/fallback.js.map +1 -1
- package/dist/provider/idle-timeout.d.ts.map +1 -1
- package/dist/provider/idle-timeout.js +5 -0
- package/dist/provider/idle-timeout.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts.map +1 -1
- package/dist/provider/model-window-suffix.js +1 -0
- package/dist/provider/model-window-suffix.js.map +1 -1
- package/dist/provider/retry.d.ts.map +1 -1
- package/dist/provider/retry.js +5 -0
- package/dist/provider/retry.js.map +1 -1
- package/dist/provider/step-stopped.d.ts +19 -0
- package/dist/provider/step-stopped.d.ts.map +1 -0
- package/dist/provider/step-stopped.js +25 -0
- package/dist/provider/step-stopped.js.map +1 -0
- package/dist/provider/token-budget.d.ts.map +1 -1
- package/dist/provider/token-budget.js +6 -1
- package/dist/provider/token-budget.js.map +1 -1
- package/dist/public-runtime.d.ts +4 -15
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +4 -15
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +0 -2
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +0 -2
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +4 -22
- package/dist/public-types.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +17 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +176 -6
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/per-call-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/per-call-review.js +4 -5
- package/dist/runtime/query/iteration/phases/per-call-review.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +6 -3
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/iteration/recorded-context.d.ts +1 -1
- package/dist/runtime/query/iteration/recorded-context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/recorded-context.js +2 -1
- package/dist/runtime/query/iteration/recorded-context.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts +9 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +48 -2
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/resume-pending.d.ts.map +1 -1
- package/dist/runtime/query/resume-pending.js +7 -4
- package/dist/runtime/query/resume-pending.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +40 -8
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +139 -24
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/turn/completion-audit.d.ts +32 -0
- package/dist/runtime/query/turn/completion-audit.d.ts.map +1 -0
- package/dist/runtime/query/turn/completion-audit.js +56 -0
- package/dist/runtime/query/turn/completion-audit.js.map +1 -0
- package/dist/runtime/query/turn/stall-monitor.d.ts +78 -0
- package/dist/runtime/query/turn/stall-monitor.d.ts.map +1 -0
- package/dist/runtime/query/turn/stall-monitor.js +101 -0
- package/dist/runtime/query/turn/stall-monitor.js.map +1 -0
- package/dist/session/paths.d.ts +1 -2
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +1 -4
- package/dist/session/paths.js.map +1 -1
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +1 -0
- package/dist/store/index.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts +2 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +5 -2
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/content-fingerprint.d.ts +13 -0
- package/dist/tools/builtins/content-fingerprint.d.ts.map +1 -1
- package/dist/tools/builtins/content-fingerprint.js +25 -0
- package/dist/tools/builtins/content-fingerprint.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +7 -2
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/shell-read-credit.d.ts +15 -0
- package/dist/tools/builtins/shell-read-credit.d.ts.map +1 -0
- package/dist/tools/builtins/shell-read-credit.js +161 -0
- package/dist/tools/builtins/shell-read-credit.js.map +1 -0
- package/dist/tools/paths.d.ts +15 -2
- package/dist/tools/paths.d.ts.map +1 -1
- package/dist/tools/paths.js +17 -2
- package/dist/tools/paths.js.map +1 -1
- package/dist/turn/evidence-recall.d.ts +1 -1
- package/dist/turn/evidence-recall.d.ts.map +1 -1
- package/dist/turn/evidence-recall.js +5 -14
- package/dist/turn/evidence-recall.js.map +1 -1
- package/dist/turn/memory-recall.d.ts.map +1 -1
- package/dist/turn/memory-recall.js +18 -4
- package/dist/turn/memory-recall.js.map +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +5 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/acp/index.d.ts +6 -0
- package/dist/types/acp/index.d.ts.map +1 -1
- package/dist/types/agent/base.d.ts +4 -0
- package/dist/types/agent/base.d.ts.map +1 -1
- package/dist/types/hitl/index.d.ts +8 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/ids/index.d.ts +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +1 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/provider/interface.d.ts +12 -0
- package/dist/types/provider/interface.d.ts.map +1 -1
- package/dist/types/session/config.d.ts +25 -0
- package/dist/types/session/config.d.ts.map +1 -1
- package/dist/types/session/events.d.ts +19 -0
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/prepare-step.d.ts +16 -0
- package/dist/types/session/prepare-step.d.ts.map +1 -1
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +3 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/session/turn.d.ts +1 -1
- package/dist/types/session/turn.d.ts.map +1 -1
- package/dist/types/session/turn.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/QueryAgent.ts +2 -0
- package/src/bridge/a2a/mapper.ts +2 -0
- package/src/bridge/acp/permission.ts +8 -0
- package/src/bridge/acp/server.ts +7 -1
- package/src/bridge/sse/mapper.ts +13 -0
- package/src/constants/tools/index.ts +27 -0
- package/src/contracts/session/api.ts +1 -0
- package/src/eval/harness-protection.ts +1 -1
- package/src/learning/cycle.ts +15 -16
- package/src/learning/guidance.ts +339 -0
- package/src/learning/skill.ts +7 -7
- package/src/learning/store.ts +5 -6
- package/src/peers/client.ts +2 -3
- package/src/peers/endpoint.ts +1 -2
- package/src/peers/protocol.ts +1 -0
- package/src/prompt/coding-agent-doctrine.ts +5 -3
- package/src/prompt/index.ts +0 -4
- package/src/provider/fallback.ts +7 -0
- package/src/provider/idle-timeout.ts +8 -0
- package/src/provider/model-window-suffix.ts +1 -0
- package/src/provider/retry.ts +8 -0
- package/src/provider/step-stopped.ts +27 -0
- package/src/provider/token-budget.ts +6 -1
- package/src/public-runtime.ts +7 -28
- package/src/public-tools.ts +0 -3
- package/src/public-types.ts +6 -137
- package/src/runtime/query/iteration/index.ts +218 -6
- package/src/runtime/query/iteration/phases/per-call-review.ts +7 -5
- package/src/runtime/query/iteration/phases/tool-review.ts +5 -3
- package/src/runtime/query/iteration/recorded-context.ts +2 -0
- package/src/runtime/query/iteration/stream-turn.ts +51 -1
- package/src/runtime/query/resume-pending.ts +11 -4
- package/src/runtime/query/review-policy.ts +157 -24
- package/src/runtime/query/turn/completion-audit.ts +71 -0
- package/src/runtime/query/turn/stall-monitor.ts +158 -0
- package/src/session/paths.ts +2 -6
- package/src/store/index.ts +2 -0
- package/src/tools/builtins/bash.ts +6 -2
- package/src/tools/builtins/content-fingerprint.ts +25 -0
- package/src/tools/builtins/edit.ts +11 -2
- package/src/tools/builtins/shell-read-credit.ts +159 -0
- package/src/tools/paths.ts +18 -2
- package/src/turn/evidence-recall.ts +7 -15
- package/src/turn/memory-recall.ts +26 -7
- package/src/turn/reporter.ts +5 -0
- package/src/types/acp/index.ts +6 -0
- package/src/types/agent/base.ts +4 -0
- package/src/types/hitl/index.ts +8 -0
- package/src/types/ids/index.ts +1 -1
- package/src/types/message/index.ts +1 -0
- package/src/types/provider/interface.ts +13 -0
- package/src/types/session/config.ts +27 -0
- package/src/types/session/events.ts +19 -0
- package/src/types/session/prepare-step.ts +17 -0
- package/src/types/session/records.ts +3 -0
- package/src/types/session/turn.ts +2 -1
- package/dist/learning/deprecated.d.ts +0 -86
- package/dist/learning/deprecated.d.ts.map +0 -1
- package/dist/learning/deprecated.js +0 -22
- package/dist/learning/deprecated.js.map +0 -1
- package/dist/manager/resident/activity.d.ts +0 -74
- package/dist/manager/resident/activity.d.ts.map +0 -1
- package/dist/manager/resident/activity.js +0 -125
- package/dist/manager/resident/activity.js.map +0 -1
- package/dist/manager/resident/agenda.d.ts +0 -167
- package/dist/manager/resident/agenda.d.ts.map +0 -1
- package/dist/manager/resident/agenda.js +0 -588
- package/dist/manager/resident/agenda.js.map +0 -1
- package/dist/manager/resident/consumption.d.ts +0 -98
- package/dist/manager/resident/consumption.d.ts.map +0 -1
- package/dist/manager/resident/consumption.js +0 -237
- package/dist/manager/resident/consumption.js.map +0 -1
- package/dist/manager/resident/delivery-window.d.ts +0 -28
- package/dist/manager/resident/delivery-window.d.ts.map +0 -1
- package/dist/manager/resident/delivery-window.js +0 -80
- package/dist/manager/resident/delivery-window.js.map +0 -1
- package/dist/manager/resident/evidence-recall.d.ts +0 -30
- package/dist/manager/resident/evidence-recall.d.ts.map +0 -1
- package/dist/manager/resident/evidence-recall.js +0 -297
- package/dist/manager/resident/evidence-recall.js.map +0 -1
- package/dist/manager/resident/history-disk.d.ts +0 -10
- package/dist/manager/resident/history-disk.d.ts.map +0 -1
- package/dist/manager/resident/history-disk.js +0 -50
- package/dist/manager/resident/history-disk.js.map +0 -1
- package/dist/manager/resident/history.d.ts +0 -115
- package/dist/manager/resident/history.d.ts.map +0 -1
- package/dist/manager/resident/history.js +0 -203
- package/dist/manager/resident/history.js.map +0 -1
- package/dist/manager/resident/host.d.ts +0 -112
- package/dist/manager/resident/host.d.ts.map +0 -1
- package/dist/manager/resident/host.js +0 -254
- package/dist/manager/resident/host.js.map +0 -1
- package/dist/manager/resident/initiative.d.ts +0 -86
- package/dist/manager/resident/initiative.d.ts.map +0 -1
- package/dist/manager/resident/initiative.js +0 -131
- package/dist/manager/resident/initiative.js.map +0 -1
- package/dist/manager/resident/loop.d.ts +0 -49
- package/dist/manager/resident/loop.d.ts.map +0 -1
- package/dist/manager/resident/loop.js +0 -75
- package/dist/manager/resident/loop.js.map +0 -1
- package/dist/manager/resident/outbox.d.ts +0 -124
- package/dist/manager/resident/outbox.d.ts.map +0 -1
- package/dist/manager/resident/outbox.js +0 -216
- package/dist/manager/resident/outbox.js.map +0 -1
- package/dist/manager/resident/proposal.d.ts +0 -65
- package/dist/manager/resident/proposal.d.ts.map +0 -1
- package/dist/manager/resident/proposal.js +0 -77
- package/dist/manager/resident/proposal.js.map +0 -1
- package/dist/manager/resident/store.d.ts +0 -108
- package/dist/manager/resident/store.d.ts.map +0 -1
- package/dist/manager/resident/store.js +0 -205
- package/dist/manager/resident/store.js.map +0 -1
- package/dist/manager/resident/tool-evidence.d.ts +0 -112
- package/dist/manager/resident/tool-evidence.d.ts.map +0 -1
- package/dist/manager/resident/tool-evidence.js +0 -289
- package/dist/manager/resident/tool-evidence.js.map +0 -1
- package/dist/prompt/resident-learning.d.ts +0 -29
- package/dist/prompt/resident-learning.d.ts.map +0 -1
- package/dist/prompt/resident-learning.js +0 -126
- package/dist/prompt/resident-learning.js.map +0 -1
- package/dist/prompt/resident-step.d.ts +0 -42
- package/dist/prompt/resident-step.d.ts.map +0 -1
- package/dist/prompt/resident-step.js +0 -137
- package/dist/prompt/resident-step.js.map +0 -1
- package/dist/tools/resident-history.d.ts +0 -12
- package/dist/tools/resident-history.d.ts.map +0 -1
- package/dist/tools/resident-history.js +0 -82
- package/dist/tools/resident-history.js.map +0 -1
- package/dist/tools/resident-tool-evidence.d.ts +0 -9
- package/dist/tools/resident-tool-evidence.d.ts.map +0 -1
- package/dist/tools/resident-tool-evidence.js +0 -61
- package/dist/tools/resident-tool-evidence.js.map +0 -1
- package/src/learning/deprecated.ts +0 -124
- package/src/manager/resident/activity.ts +0 -211
- package/src/manager/resident/agenda.ts +0 -937
- package/src/manager/resident/consumption.ts +0 -335
- package/src/manager/resident/delivery-window.ts +0 -99
- package/src/manager/resident/evidence-recall.ts +0 -369
- package/src/manager/resident/history-disk.ts +0 -63
- package/src/manager/resident/history.ts +0 -348
- package/src/manager/resident/host.ts +0 -390
- package/src/manager/resident/initiative.ts +0 -231
- package/src/manager/resident/loop.ts +0 -119
- package/src/manager/resident/outbox.ts +0 -341
- package/src/manager/resident/proposal.ts +0 -119
- package/src/manager/resident/store.ts +0 -279
- package/src/manager/resident/tool-evidence.ts +0 -455
- package/src/prompt/resident-learning.ts +0 -152
- package/src/prompt/resident-step.ts +0 -182
- package/src/tools/resident-history.ts +0 -88
- package/src/tools/resident-tool-evidence.ts +0 -72
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
assertNativeStructuredOutputSupported,
|
|
5
5
|
} from '../../../provider/capabilities.js'
|
|
6
6
|
import { ProviderRequestError, isProviderRequestError } from '../../../provider/errors.js'
|
|
7
|
+
import { StepStopped } from '../../../provider/step-stopped.js'
|
|
7
8
|
import { StreamTextAccumulator } from '../../../provider/stream-text.js'
|
|
8
9
|
import {
|
|
9
10
|
INTERLEAVED_TOOL_INPUT_PARSE_ERROR,
|
|
@@ -120,6 +121,8 @@ export interface StreamingTurnResult {
|
|
|
120
121
|
messageId: import('../../../types/ids/index.js').MessageId
|
|
121
122
|
/** Captured only for host review, at the provider-chain dispatch boundary. */
|
|
122
123
|
requestMessages?: readonly Message[]
|
|
124
|
+
/** The step was cut short because its reasoning reached the cap before it acted. */
|
|
125
|
+
reasoningCapped?: boolean
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
/**
|
|
@@ -249,6 +252,12 @@ export async function* streamProviderTurn(
|
|
|
249
252
|
captureRequest = false,
|
|
250
253
|
/** The session the turn belongs to, named on the chat span as `gen_ai.conversation.id`. */
|
|
251
254
|
sessionId?: import('../../../types/ids/index.js').SessionId,
|
|
255
|
+
/**
|
|
256
|
+
* The most characters of reasoning this step may stream before it has made a
|
|
257
|
+
* call. When it is reached the request is stopped on purpose (the abort carries
|
|
258
|
+
* a {@link StepStopped} reason) and the step ends with what it produced so far.
|
|
259
|
+
*/
|
|
260
|
+
reasoningCapChars?: number,
|
|
252
261
|
): AsyncGenerator<SessionEvent, StreamingTurnResult> {
|
|
253
262
|
assertNativeStructuredOutputSupported(provider, params)
|
|
254
263
|
assertHostedWebSearchSupported(provider, params)
|
|
@@ -355,8 +364,20 @@ export async function* streamProviderTurn(
|
|
|
355
364
|
/** The stream broke tool-call framing; nothing it sent becomes a tool call. */
|
|
356
365
|
let framingViolated = false
|
|
357
366
|
|
|
367
|
+
// The step's own stop: aborting it ends the request without being a caller's
|
|
368
|
+
// Stop, so the turn is not cancelled by it.
|
|
369
|
+
const stepStop = new AbortController()
|
|
370
|
+
let reasoningCapped = false
|
|
371
|
+
let reasoningStreamed = 0
|
|
358
372
|
const streamParams = {
|
|
359
373
|
...params,
|
|
374
|
+
...(reasoningCapChars !== undefined
|
|
375
|
+
? {
|
|
376
|
+
signal: params.signal
|
|
377
|
+
? AbortSignal.any([params.signal, stepStop.signal])
|
|
378
|
+
: stepStop.signal,
|
|
379
|
+
}
|
|
380
|
+
: {}),
|
|
360
381
|
stream: true,
|
|
361
382
|
} satisfies import('../../../types/provider/index.js').ChatCompletionParams
|
|
362
383
|
let requestMessages: readonly Message[] | undefined
|
|
@@ -420,6 +441,7 @@ export async function* streamProviderTurn(
|
|
|
420
441
|
finishDetail = undefined
|
|
421
442
|
refusal = undefined
|
|
422
443
|
streamedLength = 0
|
|
444
|
+
reasoningStreamed = 0
|
|
423
445
|
lastOutputCall = undefined
|
|
424
446
|
usage = {
|
|
425
447
|
promptTokens: 0,
|
|
@@ -554,6 +576,7 @@ export async function* streamProviderTurn(
|
|
|
554
576
|
if (reasoning.text) {
|
|
555
577
|
bucket.text += reasoning.text
|
|
556
578
|
streamedLength += reasoning.text.length
|
|
579
|
+
reasoningStreamed += reasoning.text.length
|
|
557
580
|
await emitEvent({
|
|
558
581
|
type: 'reasoning_delta',
|
|
559
582
|
turnId,
|
|
@@ -576,6 +599,28 @@ export async function* streamProviderTurn(
|
|
|
576
599
|
})
|
|
577
600
|
yield* drainPending()
|
|
578
601
|
}
|
|
602
|
+
// Reasoning that reaches the cap before any call has begun ends the step.
|
|
603
|
+
if (
|
|
604
|
+
reasoningCapChars !== undefined &&
|
|
605
|
+
reasoningStreamed >= reasoningCapChars &&
|
|
606
|
+
toolBuckets.size === 0
|
|
607
|
+
) {
|
|
608
|
+
reasoningCapped = true
|
|
609
|
+
if (!reasoning.done) {
|
|
610
|
+
await emitEvent({
|
|
611
|
+
type: 'reasoning_completed',
|
|
612
|
+
turnId,
|
|
613
|
+
iteration,
|
|
614
|
+
messageId,
|
|
615
|
+
blockIndex: reasoning.index,
|
|
616
|
+
...(bucket.text ? { text: bucket.text } : {}),
|
|
617
|
+
signed: bucket.signature !== undefined,
|
|
618
|
+
})
|
|
619
|
+
yield* drainPending()
|
|
620
|
+
}
|
|
621
|
+
stepStop.abort(new StepStopped())
|
|
622
|
+
break
|
|
623
|
+
}
|
|
579
624
|
}
|
|
580
625
|
|
|
581
626
|
if (chunk.delta.content) {
|
|
@@ -1091,5 +1136,10 @@ export async function* streamProviderTurn(
|
|
|
1091
1136
|
chatSpan.setStatus({ code: SpanStatusCode.OK })
|
|
1092
1137
|
chatSpan.end()
|
|
1093
1138
|
|
|
1094
|
-
return {
|
|
1139
|
+
return {
|
|
1140
|
+
response,
|
|
1141
|
+
messageId,
|
|
1142
|
+
...(requestMessages ? { requestMessages } : {}),
|
|
1143
|
+
...(reasoningCapped ? { reasoningCapped } : {}),
|
|
1144
|
+
}
|
|
1095
1145
|
}
|
|
@@ -248,7 +248,7 @@ function derivePartialDenials(
|
|
|
248
248
|
for (const call of asked) {
|
|
249
249
|
const mods = decision.modifications.filter((mod) => mod.toolCallId === call.id)
|
|
250
250
|
if (mods.some((mod) => mod.action === 'deny'))
|
|
251
|
-
denials.set(call.id, DECLINED_TOOL_CALL_FEEDBACK)
|
|
251
|
+
denials.set(call.id, decision.policyRefusals?.[call.id] ?? DECLINED_TOOL_CALL_FEEDBACK)
|
|
252
252
|
else if (!mods.some((mod) => mod.action === 'approve')) {
|
|
253
253
|
denials.set(
|
|
254
254
|
call.id,
|
|
@@ -280,7 +280,8 @@ function derivePartialDeclines(
|
|
|
280
280
|
const denied = asked.filter(
|
|
281
281
|
(call) =>
|
|
282
282
|
!gateDenied.has(call.id) &&
|
|
283
|
-
decision.modifications.some((mod) => mod.toolCallId === call.id && mod.action === 'deny')
|
|
283
|
+
decision.modifications.some((mod) => mod.toolCallId === call.id && mod.action === 'deny') &&
|
|
284
|
+
decision.policyRefusals?.[call.id] === undefined,
|
|
284
285
|
)
|
|
285
286
|
if (denied.length > 0) return new Map(denied.map((call) => [call.id, {}]))
|
|
286
287
|
}
|
|
@@ -833,7 +834,10 @@ function derriveDeclines(
|
|
|
833
834
|
}
|
|
834
835
|
if (decision.action === 'modify_tools') {
|
|
835
836
|
const denied = decision.modifications.filter(
|
|
836
|
-
(mod) =>
|
|
837
|
+
(mod) =>
|
|
838
|
+
mod.action === 'deny' &&
|
|
839
|
+
!gateDenied.has(mod.toolCallId) &&
|
|
840
|
+
decision.policyRefusals?.[mod.toolCallId] === undefined,
|
|
837
841
|
)
|
|
838
842
|
if (denied.length > 0) return new Map(denied.map((mod) => [mod.toolCallId, {}]))
|
|
839
843
|
}
|
|
@@ -868,7 +872,10 @@ function derriveDenials(
|
|
|
868
872
|
const denials = new Map<string, string>()
|
|
869
873
|
for (const mod of decision.modifications) {
|
|
870
874
|
if (mod.action === 'deny') {
|
|
871
|
-
denials.set(
|
|
875
|
+
denials.set(
|
|
876
|
+
mod.toolCallId,
|
|
877
|
+
decision.policyRefusals?.[mod.toolCallId] ?? DECLINED_TOOL_CALL_FEEDBACK,
|
|
878
|
+
)
|
|
872
879
|
}
|
|
873
880
|
}
|
|
874
881
|
for (const mod of decision.modifications) {
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* person (`prompt`).
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
import { homedir } from 'node:os'
|
|
25
|
+
import { dirname, parse } from 'node:path'
|
|
26
|
+
|
|
24
27
|
import { unknownProgramInLine } from '../../authorization/program.js'
|
|
25
28
|
import type { ShellWord } from '../../authorization/shell-lexer.js'
|
|
26
29
|
import {
|
|
@@ -38,6 +41,7 @@ import type {
|
|
|
38
41
|
ResumeHandler,
|
|
39
42
|
ToolCallDeclined,
|
|
40
43
|
ToolCallSummary,
|
|
44
|
+
ToolModification,
|
|
41
45
|
} from '../../types/hitl/index.js'
|
|
42
46
|
import type { ApprovalPolicy } from '../../types/hitl/policy.js'
|
|
43
47
|
import type { SessionId, TurnId } from '../../types/ids/index.js'
|
|
@@ -242,7 +246,7 @@ function onlyReadsOutsideRoots(
|
|
|
242
246
|
* one that needed a person is dropped would run a plan with a hole in it.
|
|
243
247
|
*/
|
|
244
248
|
export const SANDBOX_ESCAPE_UNATTENDED_REFUSAL =
|
|
245
|
-
'Refused:
|
|
249
|
+
'Refused: this call asks to run outside the sandbox, which needs a person to confirm it each time, and nobody can be asked in this session. This call did not run; the other calls in the batch are not affected. Run the command inside the sandbox instead.'
|
|
246
250
|
|
|
247
251
|
/**
|
|
248
252
|
* What the model is told when a batch reaches a path outside the turn's roots
|
|
@@ -254,7 +258,7 @@ export const SANDBOX_ESCAPE_UNATTENDED_REFUSAL =
|
|
|
254
258
|
* the CLI's `--add-dir`), where an operator wrote the directory down.
|
|
255
259
|
*/
|
|
256
260
|
export const OUTSIDE_ROOTS_UNATTENDED_REFUSAL =
|
|
257
|
-
"Refused:
|
|
261
|
+
"Refused: this call reaches a path outside the working directory and the directories added to this session, which needs a person to approve it each time, and nobody can be asked in this session. This call did not run; the other calls in the batch are not affected. Use a path inside the working directory, or tell the user which directory you need so they can start the session with it added (the CLI's --add-dir)."
|
|
258
262
|
|
|
259
263
|
/**
|
|
260
264
|
* What the model is told when a batch carries a call the tool itself
|
|
@@ -265,7 +269,7 @@ export const OUTSIDE_ROOTS_UNATTENDED_REFUSAL =
|
|
|
265
269
|
* remembered grant can stand in for the person it names.
|
|
266
270
|
*/
|
|
267
271
|
export const REQUIRES_APPROVAL_UNATTENDED_REFUSAL =
|
|
268
|
-
"Refused:
|
|
272
|
+
"Refused: this call is one the tool itself declared always needs a person's approval, and nobody can be asked in this session. This call did not run; the other calls in the batch are not affected. This cannot be granted by a rule, an automation mode, a remembered approval or a skill; run it in a session where a person can be asked."
|
|
269
273
|
|
|
270
274
|
/**
|
|
271
275
|
* What the model is told when a batch runs a command whose own program name
|
|
@@ -277,7 +281,7 @@ export const REQUIRES_APPROVAL_UNATTENDED_REFUSAL =
|
|
|
277
281
|
* "unattended allow" for this.
|
|
278
282
|
*/
|
|
279
283
|
export const UNKNOWN_PROGRAM_UNATTENDED_REFUSAL =
|
|
280
|
-
'Refused:
|
|
284
|
+
'Refused: this call runs a command whose own program name is decided at runtime (a substitution or a variable stands for it), which needs a person to approve it each time, and nobody can be asked in this session. This call did not run; the other calls in the batch are not affected. Write the program name literally; compute what needs computing in the arguments instead.'
|
|
281
285
|
|
|
282
286
|
/**
|
|
283
287
|
* What the model is told when a batch writes a protected path (a git hook, a
|
|
@@ -289,7 +293,7 @@ export const UNKNOWN_PROGRAM_UNATTENDED_REFUSAL =
|
|
|
289
293
|
* existed (a mode, a rule, "approve all") is not an answer about it.
|
|
290
294
|
*/
|
|
291
295
|
export const PROTECTED_PATH_UNATTENDED_REFUSAL =
|
|
292
|
-
"Refused:
|
|
296
|
+
"Refused: this call writes a protected path (a git hook or config, a shell startup file, editor or tool configuration, or the project's .namzu directory), which needs a person to approve it each time, and nobody can be asked in this session. This call did not run; the other calls in the batch are not affected. Leave those files alone, or tell the user what change you need so they can make it."
|
|
293
297
|
|
|
294
298
|
/**
|
|
295
299
|
* What the model is told when a batch removes something catastrophic (the
|
|
@@ -297,21 +301,21 @@ export const PROTECTED_PATH_UNATTENDED_REFUSAL =
|
|
|
297
301
|
* across directories, a target a variable may empty) and nobody can be asked.
|
|
298
302
|
*/
|
|
299
303
|
export const DANGEROUS_REMOVAL_UNATTENDED_REFUSAL =
|
|
300
|
-
'Refused:
|
|
304
|
+
'Refused: this call removes something this check treats as catastrophic (the file system root, a top-level or home directory, the working directory or a parent of it, a glob across directories, or a target a variable or substitution may leave empty), which needs a person to approve it each time, and nobody can be asked in this session. This call did not run; the other calls in the batch are not affected. Name the exact directory to remove with a literal path.'
|
|
301
305
|
|
|
302
306
|
/**
|
|
303
307
|
* What the model is told when a batch carries a call a rule or a hook asked a
|
|
304
308
|
* person to approve and nobody can be asked.
|
|
305
309
|
*/
|
|
306
310
|
export const ASK_RULE_UNATTENDED_REFUSAL =
|
|
307
|
-
'Refused:
|
|
311
|
+
'Refused: this call is one a rule or a hook says a person must approve, and nobody can be asked in this session. This call did not run; the other calls in the batch are not affected. Run it in a session where a person can be asked, or have the operator change the rule.'
|
|
308
312
|
|
|
309
313
|
/**
|
|
310
314
|
* What the model is told when the checks that decide whether a call reaches
|
|
311
315
|
* past its boundary did not finish and nobody can be asked.
|
|
312
316
|
*/
|
|
313
317
|
export const UNFINISHED_CHECK_UNATTENDED_REFUSAL =
|
|
314
|
-
'Refused: the safety checks on
|
|
318
|
+
'Refused: the safety checks on this call did not finish, so it cannot be shown to be safe, and a person has to approve it and nobody can be asked in this session. This call did not run; the other calls in the batch are not affected. Try a simpler form of the call.'
|
|
315
319
|
|
|
316
320
|
/** What the model is told when the conversation is too long for the classifier to read. */
|
|
317
321
|
export const CLASSIFIER_TRANSCRIPT_TOO_LONG_FEEDBACK =
|
|
@@ -534,6 +538,37 @@ export interface ScreenConsentRecord {
|
|
|
534
538
|
readonly sessions: Set<string>
|
|
535
539
|
}
|
|
536
540
|
|
|
541
|
+
/** The directory to name for a path the guard stopped: the path itself unless it looks like a file. */
|
|
542
|
+
function directoryToAllow(path: string): string {
|
|
543
|
+
const last = path.split(/[\\/]/).pop() ?? ''
|
|
544
|
+
return last.includes('.') ? dirname(path) : path
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** The outside-the-roots refusal, naming the directory and the way to allow it. */
|
|
548
|
+
export function outsideRootsRefusal(toolCalls: readonly ToolCallSummary[]): string {
|
|
549
|
+
const paths = [...new Set(toolCalls.flatMap((tc) => tc.escalation?.outsidePaths ?? []))]
|
|
550
|
+
if (paths.length === 0) return OUTSIDE_ROOTS_UNATTENDED_REFUSAL
|
|
551
|
+
const blocked = `${OUTSIDE_ROOTS_UNATTENDED_REFUSAL} Blocked: ${paths.slice(0, 3).join(', ')}.`
|
|
552
|
+
const directories = [...new Set(paths.map(directoryToAllow))]
|
|
553
|
+
// A directory that is the file system root or the home directory is too wide to
|
|
554
|
+
// suggest adding; a path directly under the root is more often a mistyped name.
|
|
555
|
+
const wide = new Set([parse(directories[0] ?? '/').root, homedir()])
|
|
556
|
+
const addable = directories.filter((d) => !wide.has(d)).slice(0, 3)
|
|
557
|
+
if (addable.length === 0) {
|
|
558
|
+
return `${blocked} It sits directly under a directory too wide to add; if the name is a typo, use a path inside the working directory or the system temp directory.`
|
|
559
|
+
}
|
|
560
|
+
return `${blocked} To allow it, the user starts the session with ${addable.map((d) => `--add-dir ${d}`).join(' ')}.`
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** The catastrophic-removal refusal with the one-line reason the check found. */
|
|
564
|
+
export function dangerousRemovalRefusal(toolCalls: readonly ToolCallSummary[]): string {
|
|
565
|
+
const why = toolCalls.find((tc) => tc.escalation?.dangerousRemoval !== undefined)?.escalation
|
|
566
|
+
?.dangerousRemoval
|
|
567
|
+
return why === undefined
|
|
568
|
+
? DANGEROUS_REMOVAL_UNATTENDED_REFUSAL
|
|
569
|
+
: `${DANGEROUS_REMOVAL_UNATTENDED_REFUSAL} Why: ${why}.`
|
|
570
|
+
}
|
|
571
|
+
|
|
537
572
|
/**
|
|
538
573
|
* Whether the call writes a protected path, removes something catastrophic, or
|
|
539
574
|
* could not be checked.
|
|
@@ -552,7 +587,7 @@ function floorRefusal(toolCalls: readonly ToolCallSummary[]): string {
|
|
|
552
587
|
return PROTECTED_PATH_UNATTENDED_REFUSAL
|
|
553
588
|
}
|
|
554
589
|
return toolCalls.some((tc) => tc.escalation?.dangerousRemoval !== undefined)
|
|
555
|
-
?
|
|
590
|
+
? dangerousRemovalRefusal(toolCalls)
|
|
556
591
|
: UNFINISHED_CHECK_UNATTENDED_REFUSAL
|
|
557
592
|
}
|
|
558
593
|
|
|
@@ -589,6 +624,39 @@ function isBypassImmune(tc: ToolCallSummary): boolean {
|
|
|
589
624
|
)
|
|
590
625
|
}
|
|
591
626
|
|
|
627
|
+
/** The slice of a reviewed call {@link mustAskPerson} reads; a `ToolCallSummary` fits it. */
|
|
628
|
+
export interface MustAskCall {
|
|
629
|
+
readonly escalation?: unknown
|
|
630
|
+
readonly requiresApproval?: boolean
|
|
631
|
+
readonly authorization?: { readonly explicitReview?: boolean; readonly askRule?: boolean }
|
|
632
|
+
readonly hookPermission?: { readonly decision?: string }
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Whether a standing "yes" (a remembered `approve-all`, or a host's own
|
|
637
|
+
* "allow for this conversation") must not cover this call: it exists to put a
|
|
638
|
+
* question to a person.
|
|
639
|
+
*
|
|
640
|
+
* It crosses the turn's boundary in any way (a sandbox escape, a path outside
|
|
641
|
+
* the roots, a protected path, a catastrophic removal, a program nobody can
|
|
642
|
+
* name, a check that did not finish), its tool needs a yes on every call, or a
|
|
643
|
+
* rule or a hook asked for a person. `escalation` is tested as a whole rather
|
|
644
|
+
* than field by field, so a kind of escalation added later is asked about
|
|
645
|
+
* before anyone remembers to list it. A call that reaches the first look at
|
|
646
|
+
* the screen is asked about too; that is a property of the request
|
|
647
|
+
* (`ToolReviewRequest.screenConsent`), so a host adds it where it builds the
|
|
648
|
+
* ask.
|
|
649
|
+
*/
|
|
650
|
+
export function mustAskPerson(call: MustAskCall): boolean {
|
|
651
|
+
return (
|
|
652
|
+
call.escalation !== undefined ||
|
|
653
|
+
call.requiresApproval === true ||
|
|
654
|
+
call.authorization?.explicitReview === true ||
|
|
655
|
+
call.authorization?.askRule === true ||
|
|
656
|
+
call.hookPermission?.decision === 'ask'
|
|
657
|
+
)
|
|
658
|
+
}
|
|
659
|
+
|
|
592
660
|
/** The refusal for the first immune thing in a batch nobody can be asked about, or undefined. */
|
|
593
661
|
function immuneRefusal(
|
|
594
662
|
toolCalls: readonly ToolCallSummary[],
|
|
@@ -601,7 +669,7 @@ function immuneRefusal(
|
|
|
601
669
|
return PROTECTED_PATH_UNATTENDED_REFUSAL
|
|
602
670
|
}
|
|
603
671
|
if (toolCalls.some((tc) => tc.escalation?.dangerousRemoval !== undefined)) {
|
|
604
|
-
return
|
|
672
|
+
return dangerousRemovalRefusal(toolCalls)
|
|
605
673
|
}
|
|
606
674
|
if (toolCalls.some((tc) => tc.escalation?.unfinishedCheck !== undefined)) {
|
|
607
675
|
return UNFINISHED_CHECK_UNATTENDED_REFUSAL
|
|
@@ -994,7 +1062,20 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
994
1062
|
return acceptEditsAllows(tc, sessionId)
|
|
995
1063
|
}
|
|
996
1064
|
|
|
997
|
-
|
|
1065
|
+
// Decisions that refuse one call and say so. The wrapper below splits a batch on
|
|
1066
|
+
// this mark, never on the wording of the refusal.
|
|
1067
|
+
const perCallRefusals = new WeakSet<object>()
|
|
1068
|
+
const callRefusal = (feedback: string): HITLResumeDecision => {
|
|
1069
|
+
const decision: HITLResumeDecision = { action: 'reject_tools', feedback }
|
|
1070
|
+
perCallRefusals.add(decision)
|
|
1071
|
+
return decision
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const decide = async (
|
|
1075
|
+
request: Parameters<ResumeHandler>[0],
|
|
1076
|
+
context?: ResumeContext,
|
|
1077
|
+
classified = false,
|
|
1078
|
+
): Promise<HITLResumeDecision> => {
|
|
998
1079
|
if (request.type !== 'tool_review') {
|
|
999
1080
|
return request.type === 'plan_approval' ? { action: 'approve_plan' } : { action: 'continue' }
|
|
1000
1081
|
}
|
|
@@ -1128,7 +1209,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1128
1209
|
}
|
|
1129
1210
|
const live = request.toolCalls.filter((tc) => tc.authorization?.decision !== 'deny')
|
|
1130
1211
|
const escapes = live.filter((tc) => tc.escalation?.sandboxEscape === true).map((tc) => tc.id)
|
|
1131
|
-
if (mode === 'classify') {
|
|
1212
|
+
if (mode === 'classify' && !classified) {
|
|
1132
1213
|
const classifier = options.classifier as AutoReviewClassifier
|
|
1133
1214
|
const transcript = context?.transcript() ?? []
|
|
1134
1215
|
for (const tc of live) {
|
|
@@ -1180,7 +1261,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1180
1261
|
if (!live.some(isBypassImmune)) return { action: 'approve_tools' }
|
|
1181
1262
|
if (!prompt) {
|
|
1182
1263
|
const refusal = immuneRefusal(live, options.unattendedSandboxEscape === 'allow')
|
|
1183
|
-
if (refusal !== undefined) return
|
|
1264
|
+
if (refusal !== undefined) return callRefusal(refusal)
|
|
1184
1265
|
return {
|
|
1185
1266
|
action: 'approve_tools',
|
|
1186
1267
|
...(escapes.length > 0 ? { confirmedEscalations: escapes } : {}),
|
|
@@ -1200,22 +1281,22 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1200
1281
|
if (escapes.length > 0) {
|
|
1201
1282
|
if (!prompt) {
|
|
1202
1283
|
if (options.unattendedSandboxEscape !== 'allow') {
|
|
1203
|
-
return
|
|
1284
|
+
return callRefusal(SANDBOX_ESCAPE_UNATTENDED_REFUSAL)
|
|
1204
1285
|
}
|
|
1205
1286
|
// This opt-in confirms only the sandbox escape. A different call in
|
|
1206
1287
|
// the batch, or another boundary on this same call, still needs a
|
|
1207
1288
|
// person; the early return must not approve that boundary too.
|
|
1208
1289
|
if (request.toolCalls.some(reachesAFloor)) {
|
|
1209
|
-
return
|
|
1290
|
+
return callRefusal(floorRefusal(request.toolCalls))
|
|
1210
1291
|
}
|
|
1211
1292
|
if (request.toolCalls.some((tc) => (tc.escalation?.outsidePaths?.length ?? 0) > 0)) {
|
|
1212
|
-
return
|
|
1293
|
+
return callRefusal(outsideRootsRefusal(request.toolCalls))
|
|
1213
1294
|
}
|
|
1214
1295
|
if (request.toolCalls.some((tc) => tc.requiresApproval === true)) {
|
|
1215
|
-
return
|
|
1296
|
+
return callRefusal(REQUIRES_APPROVAL_UNATTENDED_REFUSAL)
|
|
1216
1297
|
}
|
|
1217
1298
|
if (request.toolCalls.some((tc) => tc.escalation?.unknownProgram !== undefined)) {
|
|
1218
|
-
return
|
|
1299
|
+
return callRefusal(UNKNOWN_PROGRAM_UNATTENDED_REFUSAL)
|
|
1219
1300
|
}
|
|
1220
1301
|
return { action: 'approve_tools', confirmedEscalations: escapes }
|
|
1221
1302
|
}
|
|
@@ -1238,7 +1319,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1238
1319
|
// far: `auto` and a remembered "approve all" were answers given before
|
|
1239
1320
|
// the call existed. With nobody to ask it is refused, not approved.
|
|
1240
1321
|
if (request.toolCalls.some(reachesAFloor)) {
|
|
1241
|
-
if (!prompt) return
|
|
1322
|
+
if (!prompt) return callRefusal(floorRefusal(request.toolCalls))
|
|
1242
1323
|
const answer = await ask()
|
|
1243
1324
|
if (answer.kind === 'reject') {
|
|
1244
1325
|
return {
|
|
@@ -1257,7 +1338,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1257
1338
|
// given about tools, before this path was named, and neither saw it.
|
|
1258
1339
|
// With nobody to ask it is refused, not approved.
|
|
1259
1340
|
if (request.toolCalls.some((tc) => (tc.escalation?.outsidePaths?.length ?? 0) > 0)) {
|
|
1260
|
-
if (!prompt) return
|
|
1341
|
+
if (!prompt) return callRefusal(outsideRootsRefusal(request.toolCalls))
|
|
1261
1342
|
const answer = await ask()
|
|
1262
1343
|
if (answer.kind === 'reject') {
|
|
1263
1344
|
return {
|
|
@@ -1278,7 +1359,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1278
1359
|
// declaration travels with the tool, and no rule, mode or remembered
|
|
1279
1360
|
// grant was ever an answer to IT specifically.
|
|
1280
1361
|
if (request.toolCalls.some((tc) => tc.requiresApproval === true)) {
|
|
1281
|
-
if (!prompt) return
|
|
1362
|
+
if (!prompt) return callRefusal(REQUIRES_APPROVAL_UNATTENDED_REFUSAL)
|
|
1282
1363
|
const answer = await ask()
|
|
1283
1364
|
if (answer.kind === 'reject') {
|
|
1284
1365
|
return {
|
|
@@ -1299,7 +1380,7 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1299
1380
|
// escape — an operator who wants this can only write the program
|
|
1300
1381
|
// name literally.
|
|
1301
1382
|
if (request.toolCalls.some((tc) => tc.escalation?.unknownProgram !== undefined)) {
|
|
1302
|
-
if (!prompt) return
|
|
1383
|
+
if (!prompt) return callRefusal(UNKNOWN_PROGRAM_UNATTENDED_REFUSAL)
|
|
1303
1384
|
const answer = await ask()
|
|
1304
1385
|
if (answer.kind === 'reject') {
|
|
1305
1386
|
return {
|
|
@@ -1312,7 +1393,10 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1312
1393
|
if (answer.kind === 'approve-all') remembered.all = true
|
|
1313
1394
|
return { action: 'approve_tools' }
|
|
1314
1395
|
}
|
|
1315
|
-
|
|
1396
|
+
// A remembered "approve all" answers ordinary calls. A call a rule or a
|
|
1397
|
+
// hook asked a person about exists to put its own question, which the
|
|
1398
|
+
// latch was never an answer to (`mustAskPerson`): it goes on to ask.
|
|
1399
|
+
if (mode === 'auto' || !prompt || (remembered.all && !request.toolCalls.some(mustAskPerson))) {
|
|
1316
1400
|
return { action: 'approve_tools' }
|
|
1317
1401
|
}
|
|
1318
1402
|
// Every call a person would be asked about is one a skill loaded in
|
|
@@ -1350,7 +1434,10 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1350
1434
|
case 'approve':
|
|
1351
1435
|
return { action: 'approve_tools' }
|
|
1352
1436
|
case 'approve-all':
|
|
1353
|
-
|
|
1437
|
+
// A rule's or a hook's question was put to this person once; an
|
|
1438
|
+
// "approve all" to it approves it and leaves the latch as it was,
|
|
1439
|
+
// as the ACP bridge does.
|
|
1440
|
+
if (!request.toolCalls.some(mustAskPerson)) remembered.all = true
|
|
1354
1441
|
return { action: 'approve_tools' }
|
|
1355
1442
|
case 'reject':
|
|
1356
1443
|
return {
|
|
@@ -1361,6 +1448,52 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
1361
1448
|
}
|
|
1362
1449
|
}
|
|
1363
1450
|
}
|
|
1451
|
+
// With nobody to ask, a refusal that belongs to one call is that call's
|
|
1452
|
+
// alone. The batch is not refused for it: each call is decided on its own,
|
|
1453
|
+
// the ones that pass run, and the one that does not gets its own reason.
|
|
1454
|
+
// Without this a single stray path in a parallel batch costs the turn every
|
|
1455
|
+
// other call in it.
|
|
1456
|
+
return async (request, context?: ResumeContext): Promise<HITLResumeDecision> => {
|
|
1457
|
+
const whole = await decide(request, context)
|
|
1458
|
+
if (
|
|
1459
|
+
whole.action !== 'reject_tools' ||
|
|
1460
|
+
prompt ||
|
|
1461
|
+
request.type !== 'tool_review' ||
|
|
1462
|
+
request.toolCalls.length < 2 ||
|
|
1463
|
+
!perCallRefusals.has(whole)
|
|
1464
|
+
) {
|
|
1465
|
+
return whole
|
|
1466
|
+
}
|
|
1467
|
+
// The whole-batch pass has already put every call to the classifier (it
|
|
1468
|
+
// refuses only after all of them were allowed), so the per-call passes do
|
|
1469
|
+
// not ask it again. One at a time: no pass races another on shared state.
|
|
1470
|
+
const alone: HITLResumeDecision[] = []
|
|
1471
|
+
for (const tc of request.toolCalls) {
|
|
1472
|
+
alone.push(await decide({ ...request, toolCalls: [tc] }, context, true))
|
|
1473
|
+
}
|
|
1474
|
+
const modifications: ToolModification[] = []
|
|
1475
|
+
const policyRefusals: Record<string, string> = {}
|
|
1476
|
+
const confirmed: string[] = []
|
|
1477
|
+
for (const [i, decision] of alone.entries()) {
|
|
1478
|
+
const id = (request.toolCalls[i] as ToolCallSummary).id
|
|
1479
|
+
if (decision.action === 'approve_tools') {
|
|
1480
|
+
modifications.push({ toolCallId: id, action: 'approve' })
|
|
1481
|
+
confirmed.push(...(decision.confirmedEscalations ?? []))
|
|
1482
|
+
} else if (decision.action === 'reject_tools' && !decision.endTurn && !decision.declined) {
|
|
1483
|
+
modifications.push({ toolCallId: id, action: 'deny' })
|
|
1484
|
+
policyRefusals[id] = decision.feedback
|
|
1485
|
+
} else {
|
|
1486
|
+
return whole
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (Object.keys(policyRefusals).length === 0) return whole
|
|
1490
|
+
return {
|
|
1491
|
+
action: 'modify_tools',
|
|
1492
|
+
modifications,
|
|
1493
|
+
policyRefusals,
|
|
1494
|
+
...(confirmed.length > 0 ? { confirmedEscalations: confirmed } : {}),
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1364
1497
|
}
|
|
1365
1498
|
|
|
1366
1499
|
/**
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The completion audit: before a turn that edited files is allowed to end,
|
|
3
|
+
* check from the turn's own record whether anything was run to check the
|
|
4
|
+
* edits. It judges what happened, not what the model says happened.
|
|
5
|
+
*
|
|
6
|
+
* It blocks at most once per turn. After the one reminder the turn may end
|
|
7
|
+
* however it likes, so the audit can never trap a run.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Tools that write to the project regardless of their input. */
|
|
11
|
+
const WRITING_TOOLS = new Set(['edit', 'write', 'apply_patch', 'notebook_edit'])
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A command that checks work: a project runner invoked with a test, lint,
|
|
15
|
+
* typecheck, format-check or build target, or a checker invoked directly.
|
|
16
|
+
*/
|
|
17
|
+
const RUNNER =
|
|
18
|
+
/\b(?:npm|pnpm|yarn|bun|npx|make|cargo|go|dotnet|gradle|gradlew|mvn|deno|just|task|uv|poetry|tox|nox|rake|bundle)\b[^|;&\n]*\b(?:test|tests|lint|check|typecheck|type-check|verify|build|vet|clippy|fmt)\b/i
|
|
19
|
+
const DIRECT =
|
|
20
|
+
/(?:^|[\s;&|(])(?:vitest|jest|pytest|mocha|tsc|eslint|biome|prettier|ruff|mypy|pyright|flake8|golangci-lint|rubocop|phpunit|node\s+--test)(?=$|[\s;&|)])/i
|
|
21
|
+
|
|
22
|
+
/** Whether a shell command is one that checks work. */
|
|
23
|
+
export function isVerificationCommand(command: string): boolean {
|
|
24
|
+
return RUNNER.test(command) || DIRECT.test(command)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Said to the model once, when it tries to end a turn that changed files and checked nothing. */
|
|
28
|
+
export const COMPLETION_AUDIT_MESSAGE =
|
|
29
|
+
"Files were changed in this turn and nothing was run afterwards to check them. Before you finish: rerun the reproduction of the defect, if there was one; run the project's tests and its lint or format check; confirm the diff holds only what was asked; name any throwaway file you made. Then give your answer, stating what you ran and what you did not run. If a check cannot be run here, say which and why."
|
|
30
|
+
|
|
31
|
+
/** A finished tool call as the audit sees it. */
|
|
32
|
+
export interface AuditedCall {
|
|
33
|
+
readonly toolName: string
|
|
34
|
+
/** The call's parsed input, when it had one. */
|
|
35
|
+
readonly input?: unknown
|
|
36
|
+
readonly isError?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class CompletionAudit {
|
|
40
|
+
private edited = false
|
|
41
|
+
private checkedSinceEdit = false
|
|
42
|
+
private blocked = false
|
|
43
|
+
|
|
44
|
+
/** Feed the calls of one finished step, in order. */
|
|
45
|
+
observe(calls: readonly AuditedCall[]): void {
|
|
46
|
+
for (const call of calls) {
|
|
47
|
+
if (call.isError) continue
|
|
48
|
+
if (WRITING_TOOLS.has(call.toolName)) {
|
|
49
|
+
this.edited = true
|
|
50
|
+
this.checkedSinceEdit = false
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
if (call.toolName === 'bash' && this.edited) {
|
|
54
|
+
const command = (call.input as { command?: unknown } | undefined)?.command
|
|
55
|
+
if (typeof command === 'string' && isVerificationCommand(command)) {
|
|
56
|
+
this.checkedSinceEdit = true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether the turn must be held back from ending. True at most once: asking
|
|
64
|
+
* records the block.
|
|
65
|
+
*/
|
|
66
|
+
takeBlock(): boolean {
|
|
67
|
+
if (this.blocked || !this.edited || this.checkedSinceEdit) return false
|
|
68
|
+
this.blocked = true
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
}
|