@namzu/sdk 37.0.0 → 38.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- 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/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +14 -1
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/config/runtime.d.ts +16 -6
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +2 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/contracts/api.d.ts +1 -1
- package/dist/contracts/api.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +10 -9
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +2 -2
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +11 -5
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/capabilities.d.ts +2 -0
- package/dist/provider/capabilities.d.ts.map +1 -1
- package/dist/provider/capabilities.js +11 -0
- package/dist/provider/capabilities.js.map +1 -1
- package/dist/provider/fallback.d.ts.map +1 -1
- package/dist/provider/fallback.js +5 -3
- package/dist/provider/fallback.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +1 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +1 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/public-types.d.ts +1 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/run/reporter.d.ts.map +1 -1
- package/dist/run/reporter.js +1 -0
- package/dist/run/reporter.js.map +1 -1
- package/dist/run/token-budget.d.ts +10 -4
- package/dist/run/token-budget.d.ts.map +1 -1
- package/dist/run/token-budget.js +45 -14
- package/dist/run/token-budget.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +3 -3
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +24 -16
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +2 -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/index.d.ts +2 -0
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +23 -2
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.d.ts.map +1 -1
- package/dist/runtime/query/iteration/stream-turn.js +11 -1
- package/dist/runtime/query/iteration/stream-turn.js.map +1 -1
- package/dist/runtime/query/observation-context.d.ts +10 -0
- package/dist/runtime/query/observation-context.d.ts.map +1 -0
- package/dist/runtime/query/observation-context.js +84 -0
- package/dist/runtime/query/observation-context.js.map +1 -0
- package/dist/runtime/query/request-context.d.ts +30 -0
- package/dist/runtime/query/request-context.d.ts.map +1 -0
- package/dist/runtime/query/request-context.js +76 -0
- package/dist/runtime/query/request-context.js.map +1 -0
- package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
- package/dist/runtime/query/request-rich-content.js +6 -3
- package/dist/runtime/query/request-rich-content.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts +3 -0
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +4 -1
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/file-walk-program.d.ts.map +1 -1
- package/dist/sandbox/file-walk-program.js +8 -0
- package/dist/sandbox/file-walk-program.js.map +1 -1
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +9 -0
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +3 -0
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/store/run/token-budget-disk.js +3 -0
- package/dist/store/run/token-budget-disk.js.map +1 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +12 -26
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/file-write-result.d.ts +4 -0
- package/dist/tools/builtins/file-write-result.d.ts.map +1 -0
- package/dist/tools/builtins/file-write-result.js +72 -0
- package/dist/tools/builtins/file-write-result.js.map +1 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +8 -1
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +10 -0
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +6 -0
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +10 -0
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/write-file.d.ts.map +1 -1
- package/dist/tools/builtins/write-file.js +30 -44
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts +4 -0
- package/dist/tools/file-read-tracker.d.ts.map +1 -0
- package/dist/tools/file-read-tracker.js +16 -0
- package/dist/tools/file-read-tracker.js.map +1 -0
- package/dist/tools/task/list.js +2 -2
- package/dist/tools/task/list.js.map +1 -1
- package/dist/types/plugin/index.d.ts +7 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/chat.d.ts +4 -0
- package/dist/types/provider/chat.d.ts.map +1 -1
- package/dist/types/provider/config.d.ts +2 -0
- package/dist/types/provider/config.d.ts.map +1 -1
- package/dist/types/provider/stream.d.ts +6 -0
- package/dist/types/provider/stream.d.ts.map +1 -1
- package/dist/types/run/config.d.ts +2 -0
- package/dist/types/run/config.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +10 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +1 -0
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +4 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bridge/a2a/mapper.ts +2 -0
- package/src/bridge/sse/mapper.ts +14 -1
- package/src/config/runtime.ts +2 -0
- package/src/contracts/api.ts +1 -0
- package/src/manager/agent/lifecycle.ts +12 -9
- package/src/prompt/coding-agent-doctrine.ts +11 -5
- package/src/provider/capabilities.ts +16 -0
- package/src/provider/fallback.ts +8 -3
- package/src/public-runtime.ts +2 -0
- package/src/public-tools.ts +2 -0
- package/src/public-types.ts +6 -0
- package/src/run/reporter.ts +1 -0
- package/src/run/token-budget.ts +51 -14
- package/src/runtime/query/executor.ts +23 -15
- package/src/runtime/query/index.ts +3 -0
- package/src/runtime/query/iteration/index.ts +28 -2
- package/src/runtime/query/iteration/stream-turn.ts +14 -1
- package/src/runtime/query/observation-context.ts +86 -0
- package/src/runtime/query/request-context.ts +103 -0
- package/src/runtime/query/request-rich-content.ts +8 -3
- package/src/runtime/query/review-policy.ts +7 -1
- package/src/runtime/query/tooling.ts +2 -0
- package/src/sandbox/file-walk-program.ts +8 -0
- package/src/sandbox/file-walk.ts +9 -0
- package/src/scheduler/completion-inbox.ts +3 -0
- package/src/store/run/token-budget-disk.ts +3 -0
- package/src/tools/builtins/edit.ts +12 -27
- package/src/tools/builtins/file-write-result.ts +84 -0
- package/src/tools/builtins/glob.ts +7 -1
- package/src/tools/builtins/grep.ts +10 -0
- package/src/tools/builtins/job.ts +8 -0
- package/src/tools/builtins/read-file.ts +10 -0
- package/src/tools/builtins/write-file.ts +39 -44
- package/src/tools/file-read-tracker.ts +16 -0
- package/src/tools/task/list.ts +2 -2
- package/src/types/plugin/index.ts +10 -0
- package/src/types/provider/chat.ts +2 -0
- package/src/types/provider/config.ts +2 -0
- package/src/types/provider/stream.ts +7 -0
- package/src/types/run/config.ts +2 -0
- package/src/types/run/events.ts +10 -0
- package/src/types/sandbox/index.ts +1 -0
- package/src/types/tool/presentation.ts +4 -0
package/src/run/token-budget.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface TokenBudgetRequestSnapshot {
|
|
|
21
21
|
runId: RunId
|
|
22
22
|
/** Cumulative request usage already charged to the owning run. */
|
|
23
23
|
usage?: TokenUsage
|
|
24
|
+
/** No reliable final receipt; only explicit reconciliation clears this marker. */
|
|
25
|
+
unresolved?: boolean
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/** Durable authority independent of a message checkpoint's cadence. */
|
|
@@ -49,8 +51,10 @@ export interface TokenBudgetSummary {
|
|
|
49
51
|
remainingTokens: number | null
|
|
50
52
|
inFlightRequests: number
|
|
51
53
|
unsettledChildren: number
|
|
52
|
-
/**
|
|
54
|
+
/** This account is blocked by accounting failure or uncertain finite allowance. */
|
|
53
55
|
poisoned: boolean
|
|
56
|
+
/** Unresolved receipts in this subtree, including retained completion records. */
|
|
57
|
+
unresolvedRequests?: number
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
interface SharedLedger {
|
|
@@ -150,9 +154,6 @@ export function validateTokenBudgetSnapshot(value: unknown): TokenBudgetSnapshot
|
|
|
150
154
|
const accountId = id(account.id, 'account id')
|
|
151
155
|
if (typeof account.settled !== 'boolean') throw new Error('Invalid token budget settled flag')
|
|
152
156
|
const limit = count(account.limit, 'limit')
|
|
153
|
-
if (accountId !== rootAccountId && limit === 0) {
|
|
154
|
-
throw new Error('A child token budget must have a positive limit')
|
|
155
|
-
}
|
|
156
157
|
const usage: Record<string, TokenUsage> = {}
|
|
157
158
|
for (const [runId, rawUsage] of Object.entries(object(account.usage, 'account usage'))) {
|
|
158
159
|
id(runId, 'usage run id')
|
|
@@ -198,6 +199,8 @@ export function validateTokenBudgetSnapshot(value: unknown): TokenBudgetSnapshot
|
|
|
198
199
|
const request = object(raw, 'request')
|
|
199
200
|
const requestId = id(request.id, 'request id')
|
|
200
201
|
const accountId = id(request.accountId, 'request account id')
|
|
202
|
+
if (request.unresolved !== undefined && typeof request.unresolved !== 'boolean')
|
|
203
|
+
throw new Error('Invalid unresolved request marker')
|
|
201
204
|
if (!isEntityId(request.runId, 'run')) throw new Error('Invalid token budget request run id')
|
|
202
205
|
if (requestIds.has(requestId)) throw new Error('Duplicate token budget request')
|
|
203
206
|
requestIds.add(requestId)
|
|
@@ -209,6 +212,7 @@ export function validateTokenBudgetSnapshot(value: unknown): TokenBudgetSnapshot
|
|
|
209
212
|
accountId,
|
|
210
213
|
runId: request.runId,
|
|
211
214
|
...(request.usage === undefined ? {} : { usage: usageValue(request.usage) }),
|
|
215
|
+
...(request.unresolved === true ? { unresolved: true } : {}),
|
|
212
216
|
}
|
|
213
217
|
}
|
|
214
218
|
const requests = source.requests.map(parseRequest)
|
|
@@ -346,9 +350,11 @@ export class TokenBudget {
|
|
|
346
350
|
rootAccountId: state.rootAccountId,
|
|
347
351
|
rootRunId: state.rootRunId,
|
|
348
352
|
accounts: new Map(state.accounts.map((account) => [account.id, account])),
|
|
349
|
-
requests: new Map(
|
|
353
|
+
requests: new Map(
|
|
354
|
+
state.requests.map((request) => [request.id, { ...request, unresolved: true }]),
|
|
355
|
+
),
|
|
350
356
|
finishedRequests: new Map(state.completedRequests.map((request) => [request.id, request])),
|
|
351
|
-
poisoned: state.poisoned === true
|
|
357
|
+
poisoned: state.poisoned === true,
|
|
352
358
|
persistence,
|
|
353
359
|
writes: Promise.resolve(),
|
|
354
360
|
},
|
|
@@ -384,7 +390,7 @@ export class TokenBudget {
|
|
|
384
390
|
}
|
|
385
391
|
|
|
386
392
|
get remaining(): number {
|
|
387
|
-
if (this.
|
|
393
|
+
if (this.admissionBlocked() || this.node.settled) return 0
|
|
388
394
|
let cursor = this.node
|
|
389
395
|
let available = this.free(cursor)
|
|
390
396
|
while (cursor.parentId !== undefined) {
|
|
@@ -430,7 +436,7 @@ export class TokenBudget {
|
|
|
430
436
|
reserve(tokens: number): TokenBudget {
|
|
431
437
|
count(tokens, 'reservation')
|
|
432
438
|
if (this.hasRequest(this.node)) throw new Error('Token budget has an in-flight request')
|
|
433
|
-
if (tokens === 0 || tokens > this.remaining)
|
|
439
|
+
if ((tokens === 0 && this.remaining !== Number.POSITIVE_INFINITY) || tokens > this.remaining)
|
|
434
440
|
throw new Error('Token budget cannot reserve the requested tokens')
|
|
435
441
|
count(this.totals(this.node).reserved + tokens, 'reserved allowance')
|
|
436
442
|
const accountId = randomUUID()
|
|
@@ -468,7 +474,8 @@ export class TokenBudget {
|
|
|
468
474
|
}
|
|
469
475
|
|
|
470
476
|
async beginRequest(): Promise<string> {
|
|
471
|
-
if (this.
|
|
477
|
+
if (this.admissionBlocked())
|
|
478
|
+
throw new Error('Token budget has unresolved spend or an accounting failure')
|
|
472
479
|
if (this.node.settled) throw new Error('Token budget is settled')
|
|
473
480
|
if (this.remaining <= 0) throw new Error('Token budget is exhausted')
|
|
474
481
|
const runId = this.runId
|
|
@@ -504,10 +511,11 @@ export class TokenBudget {
|
|
|
504
511
|
async failRequest(requestId: string, usage?: TokenUsage): Promise<void> {
|
|
505
512
|
if (this.hasReceipt(requestId)) return this.flush()
|
|
506
513
|
const request = this.requireRequest(requestId)
|
|
507
|
-
|
|
514
|
+
request.unresolved = true
|
|
508
515
|
try {
|
|
509
516
|
if (usage !== undefined) this.recordRequestUsage(request, usage)
|
|
510
517
|
} catch (error) {
|
|
518
|
+
this.ledger.poisoned = true
|
|
511
519
|
this.persist()
|
|
512
520
|
await this.flush()
|
|
513
521
|
throw error
|
|
@@ -518,9 +526,9 @@ export class TokenBudget {
|
|
|
518
526
|
|
|
519
527
|
/**
|
|
520
528
|
* Explicit host recovery after obtaining the provider's final usage receipt.
|
|
521
|
-
* Ordinary completion never
|
|
522
|
-
*
|
|
523
|
-
*
|
|
529
|
+
* Ordinary completion never clears uncertainty. Reconciliation resolves this
|
|
530
|
+
* request after its receipt is persisted; other uncertain requests continue
|
|
531
|
+
* to constrain their shared finite allowances. Usage and settlement are retained.
|
|
524
532
|
*/
|
|
525
533
|
async reconcileRequest(requestId: string, usage: TokenUsage): Promise<void> {
|
|
526
534
|
const measured = usageValue(usage)
|
|
@@ -536,6 +544,10 @@ export class TokenBudget {
|
|
|
536
544
|
} else {
|
|
537
545
|
await this.finishRequest(requestId, measured)
|
|
538
546
|
}
|
|
547
|
+
const receipt = this.ledger.finishedRequests.get(requestId)
|
|
548
|
+
if (receipt) receipt.unresolved = false
|
|
549
|
+
this.persist()
|
|
550
|
+
await this.flush()
|
|
539
551
|
if (this.ledger.requests.size === 0 && this.ledger.persistenceError === undefined) {
|
|
540
552
|
this.ledger.poisoned = false
|
|
541
553
|
this.persist()
|
|
@@ -574,7 +586,10 @@ export class TokenBudget {
|
|
|
574
586
|
).length,
|
|
575
587
|
unsettledChildren: this.children(this.node).filter((child) => !this.totals(child).closed)
|
|
576
588
|
.length,
|
|
577
|
-
poisoned: this.
|
|
589
|
+
poisoned: this.admissionBlocked(),
|
|
590
|
+
unresolvedRequests: this.uncertainRequests().filter((request) =>
|
|
591
|
+
descendants.has(request.accountId),
|
|
592
|
+
).length,
|
|
578
593
|
}
|
|
579
594
|
}
|
|
580
595
|
|
|
@@ -595,6 +610,28 @@ export class TokenBudget {
|
|
|
595
610
|
if (this.ledger.persistenceError !== undefined) throw this.ledger.persistenceError
|
|
596
611
|
}
|
|
597
612
|
|
|
613
|
+
private uncertainRequests(): TokenBudgetRequestSnapshot[] {
|
|
614
|
+
return [...this.ledger.requests.values(), ...this.ledger.finishedRequests.values()].filter(
|
|
615
|
+
(request) => request.unresolved === true,
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
private admissionBlocked(): boolean {
|
|
619
|
+
if (this.ledger.poisoned || this.ledger.persistenceError !== undefined) return true
|
|
620
|
+
const uncertain = this.uncertainRequests()
|
|
621
|
+
if (uncertain.length === 0) return false
|
|
622
|
+
// An account cannot resume its own uncertain request, even without a cap.
|
|
623
|
+
if (uncertain.some((request) => request.accountId === this.accountId)) return true
|
|
624
|
+
let cursor = this.node
|
|
625
|
+
for (;;) {
|
|
626
|
+
if (cursor.limit > 0) {
|
|
627
|
+
const descendants = this.descendants(cursor)
|
|
628
|
+
if (uncertain.some((request) => descendants.has(request.accountId))) return true
|
|
629
|
+
}
|
|
630
|
+
if (cursor.parentId === undefined) return false
|
|
631
|
+
cursor = this.requireAccount(cursor.parentId)
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
598
635
|
private get node(): TokenBudgetAccountSnapshot {
|
|
599
636
|
return this.requireAccount(this.accountId)
|
|
600
637
|
}
|
|
@@ -11,8 +11,8 @@ import { probe as defaultProbeRegistry } from '../../probe/registry.js'
|
|
|
11
11
|
import type { ProbeEnforcement } from '../../probe/registry.js'
|
|
12
12
|
import { renderToolSchema } from '../../registry/tool/schema.js'
|
|
13
13
|
import type { ActivityStore } from '../../store/activity/memory.js'
|
|
14
|
-
import { fingerprintContent } from '../../tools/builtins/content-fingerprint.js'
|
|
15
14
|
import { SKILL_TOOL_NAME } from '../../tools/builtins/skill.js'
|
|
15
|
+
import { createFileReadTracker } from '../../tools/file-read-tracker.js'
|
|
16
16
|
import type { RunId, ToolUseId } from '../../types/ids/index.js'
|
|
17
17
|
import type { InvocationState } from '../../types/invocation/index.js'
|
|
18
18
|
import {
|
|
@@ -290,6 +290,7 @@ function parseArguments(raw: string): unknown {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
export interface ToolExecutorConfig {
|
|
293
|
+
fileReadTracker?: FileReadTracker
|
|
293
294
|
tools: ToolRegistryContract
|
|
294
295
|
runId: RunId
|
|
295
296
|
workingDirectory: string
|
|
@@ -501,20 +502,7 @@ export class ToolExecutor {
|
|
|
501
502
|
private readonly preparedBatches = new WeakSet<PreparedToolBatch>()
|
|
502
503
|
/** Set per turn by the orchestrator; see {@link setStepAllowedTools}. */
|
|
503
504
|
private stepAllowedTools?: readonly string[]
|
|
504
|
-
private readonly
|
|
505
|
-
private readonly readFingerprints: Map<string, string> = new Map()
|
|
506
|
-
private readonly fileReadTracker: FileReadTracker = {
|
|
507
|
-
recordRead: (key: string, content?: string) => {
|
|
508
|
-
this.readPaths.add(key)
|
|
509
|
-
// Only when the reader had the body. A tool that records a read
|
|
510
|
-
// without one leaves the previous fingerprint alone rather than
|
|
511
|
-
// clearing it, so a later write is still checked against the last
|
|
512
|
-
// body anyone actually saw.
|
|
513
|
-
if (content !== undefined) this.readFingerprints.set(key, fingerprintContent(content))
|
|
514
|
-
},
|
|
515
|
-
hasRead: (key: string) => this.readPaths.has(key),
|
|
516
|
-
fingerprint: (key: string) => this.readFingerprints.get(key),
|
|
517
|
-
}
|
|
505
|
+
private readonly fileReadTracker: FileReadTracker
|
|
518
506
|
|
|
519
507
|
constructor(
|
|
520
508
|
config: ToolExecutorConfig,
|
|
@@ -532,6 +520,7 @@ export class ToolExecutor {
|
|
|
532
520
|
config.readToolCallBudgetEvents,
|
|
533
521
|
)
|
|
534
522
|
}
|
|
523
|
+
this.fileReadTracker = config.fileReadTracker ?? createFileReadTracker()
|
|
535
524
|
this.config = config
|
|
536
525
|
this.activityStore = activityStore
|
|
537
526
|
this.emitEvent = emitEvent
|
|
@@ -1211,6 +1200,7 @@ export class ToolExecutor {
|
|
|
1211
1200
|
toolUseId: nestedId,
|
|
1212
1201
|
toolName: name,
|
|
1213
1202
|
result: budgeted.output,
|
|
1203
|
+
...(!budgeted.truncated ? this.resultPresentation(name, preparedInput, result) : {}),
|
|
1214
1204
|
isError: !result.success,
|
|
1215
1205
|
durationMs: Date.now() - startedAt,
|
|
1216
1206
|
outputLength: budgeted.originalLength,
|
|
@@ -1230,6 +1220,21 @@ export class ToolExecutor {
|
|
|
1230
1220
|
return visibleResult
|
|
1231
1221
|
}
|
|
1232
1222
|
|
|
1223
|
+
private resultPresentation(name: string, input: unknown, result: ToolResult) {
|
|
1224
|
+
if (!result.success) return {}
|
|
1225
|
+
try {
|
|
1226
|
+
const view = this.config.tools.get(name)?.presentResult?.(input, result)
|
|
1227
|
+
if (view?.kind !== 'diff') return {}
|
|
1228
|
+
const serialized = JSON.stringify(view)
|
|
1229
|
+
if (serialized.length > (this.config.maxToolOutputChars ?? DEFAULT_MAX_TOOL_OUTPUT_CHARS))
|
|
1230
|
+
return {}
|
|
1231
|
+
return { presentation: view }
|
|
1232
|
+
} catch {
|
|
1233
|
+
// A presentation hook must not fail an already completed mutation.
|
|
1234
|
+
return {}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1233
1238
|
private buildToolContext(
|
|
1234
1239
|
recordObservation: (observation: ToolResultObservation) => void = () => {},
|
|
1235
1240
|
): ToolContext {
|
|
@@ -1721,6 +1726,9 @@ export class ToolExecutor {
|
|
|
1721
1726
|
toolUseId: toolCall.id,
|
|
1722
1727
|
toolName,
|
|
1723
1728
|
result: output,
|
|
1729
|
+
...(!postOverride && !budgeted.truncated && rawOutput === output
|
|
1730
|
+
? this.resultPresentation(toolName, input, result)
|
|
1731
|
+
: {}),
|
|
1724
1732
|
isError: effectiveIsError,
|
|
1725
1733
|
durationMs,
|
|
1726
1734
|
// Pre-truncation size, so a host can show "returned 2.1 MB" even
|
|
@@ -157,6 +157,8 @@ import { createToolPause } from './tool-pause.js'
|
|
|
157
157
|
import { ToolingBootstrap } from './tooling.js'
|
|
158
158
|
|
|
159
159
|
export interface QueryParams {
|
|
160
|
+
/** Share observations across turns of one conversation and filesystem; otherwise run-local. */
|
|
161
|
+
fileReadTracker?: import('../../types/tool/index.js').FileReadTracker
|
|
160
162
|
/** One account shared with the task scheduler and descendant runs. */
|
|
161
163
|
budget?: TokenBudget
|
|
162
164
|
/** Canonical tree ledger; defaults to disk beside the root run. */
|
|
@@ -1731,6 +1733,7 @@ export async function* query(params: QueryParams): AsyncGenerator<RunEvent, Run>
|
|
|
1731
1733
|
// registry would answer about whatever the last run configured.
|
|
1732
1734
|
...(params.skillRegistry ? { skills: params.skillRegistry } : {}),
|
|
1733
1735
|
...(params.web ? { web: params.web } : {}),
|
|
1736
|
+
...(params.fileReadTracker ? { fileReadTracker: params.fileReadTracker } : {}),
|
|
1734
1737
|
...(params.toolTimeoutMs !== undefined ? { toolTimeoutMs: params.toolTimeoutMs } : {}),
|
|
1735
1738
|
...(params.toolRetryBackoff !== undefined
|
|
1736
1739
|
? { toolRetryBackoff: params.toolRetryBackoff }
|
|
@@ -54,7 +54,13 @@ import { toErrorMessage } from '../../../utils/error.js'
|
|
|
54
54
|
import { stableDigest } from '../../../utils/hash.js'
|
|
55
55
|
import { generateMessageId } from '../../../utils/id.js'
|
|
56
56
|
import type { ToolCallOutcome } from '../executor.js'
|
|
57
|
+
import { projectObservationContext } from '../observation-context.js'
|
|
57
58
|
import { applyLifecycleHookResults } from '../plugin-hooks.js'
|
|
59
|
+
import {
|
|
60
|
+
type RequestContextSnapshot,
|
|
61
|
+
diffRequestContext,
|
|
62
|
+
snapshotRequestContext,
|
|
63
|
+
} from '../request-context.js'
|
|
58
64
|
import {
|
|
59
65
|
DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
60
66
|
type RequestImageIdentity,
|
|
@@ -164,6 +170,14 @@ export class IterationOrchestrator {
|
|
|
164
170
|
* one process must not suppress each other's first envelope.
|
|
165
171
|
*/
|
|
166
172
|
private lastEnvelopeKey: string | undefined
|
|
173
|
+
private previousRequestContext: RequestContextSnapshot | undefined
|
|
174
|
+
|
|
175
|
+
private projectObservations(messages: Message[]): Message[] {
|
|
176
|
+
const config = this.ctx.compactionConfig
|
|
177
|
+
return config && config.strategy !== 'disabled' && config.deduplicateObservations !== false
|
|
178
|
+
? projectObservationContext(messages, this.ctx.tools, config.preserveToolResultsFrom)
|
|
179
|
+
: messages
|
|
180
|
+
}
|
|
167
181
|
/** Rich tool blocks already reported; durable history is scanned every turn. */
|
|
168
182
|
private readonly warnedRichToolResults = new Set<string>()
|
|
169
183
|
/**
|
|
@@ -614,7 +628,7 @@ export class IterationOrchestrator {
|
|
|
614
628
|
? [...baseMessages, createSystemMessage(stepPreamble)]
|
|
615
629
|
: [...baseMessages]
|
|
616
630
|
const messages = projectRequestRichContent(
|
|
617
|
-
requestHistory,
|
|
631
|
+
this.projectObservations(requestHistory),
|
|
618
632
|
this.ctx.runConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
619
633
|
)
|
|
620
634
|
await this.reportUnsupportedToolResults(messages)
|
|
@@ -664,6 +678,16 @@ export class IterationOrchestrator {
|
|
|
664
678
|
}
|
|
665
679
|
|
|
666
680
|
if (this.ctx.pluginManager) {
|
|
681
|
+
const snapshot = snapshotRequestContext(messages)
|
|
682
|
+
const context = Object.freeze({
|
|
683
|
+
snapshot,
|
|
684
|
+
...(this.previousRequestContext
|
|
685
|
+
? {
|
|
686
|
+
change: diffRequestContext(this.previousRequestContext, snapshot),
|
|
687
|
+
}
|
|
688
|
+
: {}),
|
|
689
|
+
})
|
|
690
|
+
this.previousRequestContext = snapshot
|
|
667
691
|
const hookResults = await this.ctx.pluginManager.executeHooks(
|
|
668
692
|
'pre_llm_call',
|
|
669
693
|
{
|
|
@@ -673,6 +697,7 @@ export class IterationOrchestrator {
|
|
|
673
697
|
// Built inside the guard: a run with no plugins installed
|
|
674
698
|
// pays nothing for a projection nobody reads.
|
|
675
699
|
request: Object.freeze({
|
|
700
|
+
context,
|
|
676
701
|
model: stepModel,
|
|
677
702
|
// Copied per turn, not handed over live: these are the
|
|
678
703
|
// run's own message objects, and a hook writing into
|
|
@@ -752,6 +777,7 @@ export class IterationOrchestrator {
|
|
|
752
777
|
cacheControl: { type: 'auto' },
|
|
753
778
|
...(runConfig.thinking ? { thinking: runConfig.thinking } : {}),
|
|
754
779
|
...(runConfig.effort ? { effort: runConfig.effort } : {}),
|
|
780
|
+
...(!forceFinalize && runConfig.webSearch ? { webSearch: runConfig.webSearch } : {}),
|
|
755
781
|
// Thread the run abort into the model call so a Stop tears the
|
|
756
782
|
// in-flight turn down (provider passes it to fetch; the consumer
|
|
757
783
|
// also races it). Inert when never aborted.
|
|
@@ -2424,7 +2450,7 @@ export class IterationOrchestrator {
|
|
|
2424
2450
|
),
|
|
2425
2451
|
]
|
|
2426
2452
|
const finalMessages = projectRequestRichContent(
|
|
2427
|
-
finalHistory,
|
|
2453
|
+
this.projectObservations(finalHistory),
|
|
2428
2454
|
this.ctx.runConfig.maxRequestRichContentBytes ?? DEFAULT_MAX_REQUEST_RICH_CONTENT_BYTES,
|
|
2429
2455
|
)
|
|
2430
2456
|
await this.reportUnsupportedToolResults(finalMessages)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type Span, SpanStatusCode } from '@opentelemetry/api'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
assertHostedWebSearchSupported,
|
|
4
|
+
assertNativeStructuredOutputSupported,
|
|
5
|
+
} from '../../../provider/capabilities.js'
|
|
3
6
|
import { isProviderRequestError } from '../../../provider/errors.js'
|
|
4
7
|
import { GENAI, NAMZU, chatSpanName, parentContext } from '../../../telemetry/attributes.js'
|
|
5
8
|
import {
|
|
@@ -155,6 +158,7 @@ export async function* streamProviderTurn(
|
|
|
155
158
|
},
|
|
156
159
|
): AsyncGenerator<RunEvent, StreamingTurnResult> {
|
|
157
160
|
assertNativeStructuredOutputSupported(provider, params)
|
|
161
|
+
assertHostedWebSearchSupported(provider, params)
|
|
158
162
|
// The `chat {model}` span the GenAI conventions require. There was none:
|
|
159
163
|
// `chatSpanName` existed with zero call sites, so a trace carried no LLM
|
|
160
164
|
// latency at all and the token counts landed on the iteration span
|
|
@@ -340,6 +344,15 @@ export async function* streamProviderTurn(
|
|
|
340
344
|
recordTimeToFirstToken(params.model, Date.now() - callStartedAt)
|
|
341
345
|
}
|
|
342
346
|
|
|
347
|
+
if (chunk.delta.hostedTool) {
|
|
348
|
+
await emitEvent({
|
|
349
|
+
type: 'hosted_tool',
|
|
350
|
+
runId,
|
|
351
|
+
iteration,
|
|
352
|
+
tool: chunk.delta.hostedTool,
|
|
353
|
+
})
|
|
354
|
+
yield* drainPending()
|
|
355
|
+
}
|
|
343
356
|
if (chunk.delta.citation) citations.push(chunk.delta.citation)
|
|
344
357
|
|
|
345
358
|
const reasoning = chunk.delta.reasoning
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { findRetainedIndices } from '../../compaction/retention.js'
|
|
3
|
+
import { estimateMessageTokens } from '../../compaction/token-estimate.js'
|
|
4
|
+
import { isClearedToolResult } from '../../compaction/tool-result-editing.js'
|
|
5
|
+
import type { Message, ToolMessage } from '../../types/message/index.js'
|
|
6
|
+
import type { ToolRegistryContract } from '../../types/tool/index.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Request-only exact observation masking. Every reference targets a full result
|
|
10
|
+
* in THIS projection. Recompute from canonical history, never from yesterday's
|
|
11
|
+
* mask, so removing a former representative cannot leave a dangling reference.
|
|
12
|
+
* No tool executions are skipped, and equal output is not a freshness claim.
|
|
13
|
+
*/
|
|
14
|
+
export function projectObservationContext(
|
|
15
|
+
messages: Message[],
|
|
16
|
+
tools: Pick<ToolRegistryContract, 'get'>,
|
|
17
|
+
preserveToolResultsFrom: readonly string[] = [],
|
|
18
|
+
): Message[] {
|
|
19
|
+
const calls = new Map<string, { name: string; arguments: string } | null>()
|
|
20
|
+
for (const message of messages) {
|
|
21
|
+
if (message.role !== 'assistant') continue
|
|
22
|
+
for (const call of message.toolCalls ?? []) {
|
|
23
|
+
// Ambiguous IDs cannot prove which observation belongs to which call.
|
|
24
|
+
if (calls.has(call.id)) calls.set(call.id, null)
|
|
25
|
+
else calls.set(call.id, call.metadata?.inputTruncated ? null : call.function)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const resultCounts = new Map<string, number>()
|
|
29
|
+
for (const message of messages) {
|
|
30
|
+
if (message.role === 'tool') {
|
|
31
|
+
resultCounts.set(message.toolCallId, (resultCounts.get(message.toolCallId) ?? 0) + 1)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const protectedIndices = findRetainedIndices(messages)
|
|
35
|
+
const representatives = new Map<string, ToolMessage>()
|
|
36
|
+
let projected: Message[] | undefined
|
|
37
|
+
for (let index = 0; index < messages.length; index++) {
|
|
38
|
+
const message = messages[index]
|
|
39
|
+
if (
|
|
40
|
+
!message ||
|
|
41
|
+
message.role !== 'tool' ||
|
|
42
|
+
message.isError ||
|
|
43
|
+
typeof message.content !== 'string' ||
|
|
44
|
+
message.content.length < 1024 ||
|
|
45
|
+
isClearedToolResult(message.content) ||
|
|
46
|
+
resultCounts.get(message.toolCallId) !== 1
|
|
47
|
+
)
|
|
48
|
+
continue
|
|
49
|
+
const call = calls.get(message.toolCallId)
|
|
50
|
+
if (!call || preserveToolResultsFrom.includes(call.name)) continue
|
|
51
|
+
const definition = tools.get(call.name)
|
|
52
|
+
if (!definition) continue
|
|
53
|
+
try {
|
|
54
|
+
const input = definition.inputSchema.safeParse(JSON.parse(call.arguments))
|
|
55
|
+
if (
|
|
56
|
+
!input.success ||
|
|
57
|
+
definition.isReadOnly?.(input.data) !== true ||
|
|
58
|
+
definition.isDestructive?.(input.data) === true
|
|
59
|
+
)
|
|
60
|
+
continue
|
|
61
|
+
} catch {
|
|
62
|
+
// Classification is advisory; invalid input or a broken custom predicate
|
|
63
|
+
// must leave evidence intact, not abort the request.
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
// Exact arguments intentionally: equivalent JSON with different formatting
|
|
67
|
+
// is a missed optimization, not permission to merge different file ranges.
|
|
68
|
+
const key = createHash('sha256')
|
|
69
|
+
.update(JSON.stringify([call.name, call.arguments, message.content]))
|
|
70
|
+
.digest('hex')
|
|
71
|
+
const representative = representatives.get(key)
|
|
72
|
+
if (!representative) {
|
|
73
|
+
representatives.set(key, message)
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
if (protectedIndices.has(index)) continue
|
|
77
|
+
const replacement: ToolMessage = {
|
|
78
|
+
...message,
|
|
79
|
+
content: `[Duplicate observation: this call returned exactly the same text as tool result ${JSON.stringify(representative.toolCallId)}, whose full content remains in this request. Both calls occurred. This is historical evidence, not a claim that external state is still unchanged.]`,
|
|
80
|
+
}
|
|
81
|
+
if (estimateMessageTokens(replacement) >= estimateMessageTokens(message)) continue
|
|
82
|
+
projected ??= [...messages]
|
|
83
|
+
projected[index] = replacement
|
|
84
|
+
}
|
|
85
|
+
return projected ?? messages
|
|
86
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import type { Message, MessageRole } from '../../types/message/index.js'
|
|
3
|
+
|
|
4
|
+
/** A content block present at the SDK's provider-input boundary, not a file freshness claim. */
|
|
5
|
+
export interface RequestContextPart {
|
|
6
|
+
readonly messageIndex: number
|
|
7
|
+
readonly partIndex: number
|
|
8
|
+
readonly role: MessageRole
|
|
9
|
+
readonly kind: 'text' | 'image' | 'document' | 'stored' | 'tool-call'
|
|
10
|
+
readonly digest: string
|
|
11
|
+
readonly toolCallId?: string
|
|
12
|
+
readonly isError?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RequestContextSnapshot {
|
|
16
|
+
readonly boundary: 'provider-input'
|
|
17
|
+
readonly parts: readonly RequestContextPart[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RequestContextChange {
|
|
21
|
+
/** Parts from the earlier snapshot no longer present, including replaced content. */
|
|
22
|
+
readonly removed: readonly RequestContextPart[]
|
|
23
|
+
/** Parts in the new snapshot not present before. */
|
|
24
|
+
readonly added: readonly RequestContextPart[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function canonicalJson(value: unknown): string {
|
|
28
|
+
return JSON.stringify(value, (_key, item: unknown) => {
|
|
29
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return item
|
|
30
|
+
return Object.fromEntries(Object.entries(item).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Inspect supplied messages after compaction and request projection. No content
|
|
36
|
+
* is retained in the snapshot. A digest identifies exact blocks, not semantic
|
|
37
|
+
* knowledge, token counts, file versions or provider-private reasoning replay.
|
|
38
|
+
*/
|
|
39
|
+
export function snapshotRequestContext(messages: readonly Message[]): RequestContextSnapshot {
|
|
40
|
+
const parts: RequestContextPart[] = []
|
|
41
|
+
for (const [messageIndex, message] of messages.entries()) {
|
|
42
|
+
let partIndex = 0
|
|
43
|
+
const add = (kind: RequestContextPart['kind'], payload: unknown, toolCallId?: string) => {
|
|
44
|
+
parts.push(
|
|
45
|
+
Object.freeze({
|
|
46
|
+
messageIndex,
|
|
47
|
+
partIndex: partIndex++,
|
|
48
|
+
role: message.role,
|
|
49
|
+
kind,
|
|
50
|
+
digest: createHash('sha256').update(canonicalJson(payload)).digest('hex'),
|
|
51
|
+
...(toolCallId !== undefined ? { toolCallId } : {}),
|
|
52
|
+
...(message.role === 'tool' ? { isError: message.isError === true } : {}),
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
const callId = message.role === 'tool' ? message.toolCallId : undefined
|
|
57
|
+
if (typeof message.content === 'string') add('text', message.content, callId)
|
|
58
|
+
else if (Array.isArray(message.content)) {
|
|
59
|
+
for (const block of message.content) {
|
|
60
|
+
if (block.type === 'text') add('text', block.text, callId)
|
|
61
|
+
else add(block.type, block, callId)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (message.role === 'user') {
|
|
65
|
+
for (const attachment of message.attachments ?? []) {
|
|
66
|
+
add(attachment.type ?? 'image', attachment)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (message.role === 'assistant') {
|
|
70
|
+
for (const call of message.toolCalls ?? []) {
|
|
71
|
+
add('tool-call', [call.function.name, call.function.arguments], call.id)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return Object.freeze({ boundary: 'provider-input', parts: Object.freeze(parts) })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const key = (part: RequestContextPart): string =>
|
|
79
|
+
JSON.stringify([part.role, part.kind, part.toolCallId, part.isError, part.digest])
|
|
80
|
+
|
|
81
|
+
/** Compare occurrences, not sets: removing one of two identical blocks is a removal. */
|
|
82
|
+
export function diffRequestContext(
|
|
83
|
+
previous: RequestContextSnapshot,
|
|
84
|
+
next: RequestContextSnapshot,
|
|
85
|
+
): RequestContextChange {
|
|
86
|
+
const unmatched = new Map<string, RequestContextPart[]>()
|
|
87
|
+
for (const part of previous.parts) {
|
|
88
|
+
const identity = key(part)
|
|
89
|
+
const occurrences = unmatched.get(identity) ?? []
|
|
90
|
+
occurrences.push(part)
|
|
91
|
+
unmatched.set(identity, occurrences)
|
|
92
|
+
}
|
|
93
|
+
const added: RequestContextPart[] = []
|
|
94
|
+
for (const part of next.parts) {
|
|
95
|
+
const occurrences = unmatched.get(key(part))
|
|
96
|
+
if (occurrences?.length) occurrences.pop()
|
|
97
|
+
else added.push(part)
|
|
98
|
+
}
|
|
99
|
+
const removed = [...unmatched.values()]
|
|
100
|
+
.flat()
|
|
101
|
+
.sort((a, b) => a.messageIndex - b.messageIndex || a.partIndex - b.partIndex)
|
|
102
|
+
return Object.freeze({ removed: Object.freeze(removed), added: Object.freeze(added) })
|
|
103
|
+
}
|
|
@@ -45,20 +45,25 @@ export interface RequestImageIdentity {
|
|
|
45
45
|
const userMarker = (kind: RichKind): string =>
|
|
46
46
|
`[${kind} omitted from this model request to keep the accumulated rich-content payload within its configured size limit; attach it again in a new message if it is still needed.]`
|
|
47
47
|
|
|
48
|
+
// Losing an observation does not undo the action that produced it. This
|
|
49
|
+
// projection knows the result bytes, not whether repeating its tool is safe.
|
|
50
|
+
const toolRecovery =
|
|
51
|
+
'The original result is retained in conversation history, but this content is not available in the current model request. Recover it from an available saved artifact or a read-only observation if needed. Do not repeat a state-changing action merely to recover its output.'
|
|
52
|
+
|
|
48
53
|
const toolMarker = (kind: RichKind): string =>
|
|
49
|
-
`[${kind} omitted from this model request to keep the accumulated rich-content payload within its configured size limit
|
|
54
|
+
`[${kind} omitted from this model request to keep the accumulated rich-content payload within its configured size limit. ${toolRecovery}]`
|
|
50
55
|
|
|
51
56
|
const rejectedUserImageMarker = (): string =>
|
|
52
57
|
'[image omitted from this model request because the provider rejected this image; attach a corrected image in a new message if it is still needed.]'
|
|
53
58
|
|
|
54
59
|
const rejectedToolImageMarker = (): string =>
|
|
55
|
-
|
|
60
|
+
`[image omitted from this model request because the provider rejected this image. ${toolRecovery} Correct the image source before presenting it again.]`
|
|
56
61
|
|
|
57
62
|
const invalidUserImageMarker = (): string =>
|
|
58
63
|
'[image omitted from this model request because its encoded bytes are not a complete supported raster matching the declared media type; attach a corrected image in a new message if it is still needed.]'
|
|
59
64
|
|
|
60
65
|
const invalidToolImageMarker = (): string =>
|
|
61
|
-
|
|
66
|
+
`[image omitted from this model request because the producing tool returned encoded bytes that are not a complete supported raster matching the declared media type. ${toolRecovery} Correct the image source before presenting it again.]`
|
|
62
67
|
|
|
63
68
|
function assertInlineAttachment(
|
|
64
69
|
attachment: MessageAttachment,
|
|
@@ -25,6 +25,7 @@ import type { ToolRegistry } from '../../registry/tool/execute.js'
|
|
|
25
25
|
import { isTrustedReadOnly } from '../../tools/trusted-read-only.js'
|
|
26
26
|
import type { HITLResumeDecision, ResumeHandler, ToolCallSummary } from '../../types/hitl/index.js'
|
|
27
27
|
import type { ApprovalPolicy } from '../../types/hitl/policy.js'
|
|
28
|
+
import type { RunId } from '../../types/ids/index.js'
|
|
28
29
|
import { PLAN_MODE_REFUSAL } from '../../types/permission/index.js'
|
|
29
30
|
|
|
30
31
|
export type ReviewMode =
|
|
@@ -141,6 +142,8 @@ export function batchNeedsReview(
|
|
|
141
142
|
|
|
142
143
|
/** The batch a person is asked about. */
|
|
143
144
|
export interface ToolReviewRequest {
|
|
145
|
+
/** Originating run, preserved by createReviewHandler for host attribution. */
|
|
146
|
+
readonly runId?: RunId
|
|
144
147
|
readonly toolCalls: readonly ToolCallSummary[]
|
|
145
148
|
}
|
|
146
149
|
|
|
@@ -204,7 +207,10 @@ export function createReviewHandler(options: ReviewPolicyOptions = {}): ResumeHa
|
|
|
204
207
|
if (mode === 'auto' || !prompt || remembered.all) {
|
|
205
208
|
return { action: 'approve_tools' }
|
|
206
209
|
}
|
|
207
|
-
const answer = await prompt({
|
|
210
|
+
const answer = await prompt({
|
|
211
|
+
runId: request.runId,
|
|
212
|
+
toolCalls: request.toolCalls,
|
|
213
|
+
})
|
|
208
214
|
switch (answer.kind) {
|
|
209
215
|
case 'approve':
|
|
210
216
|
return { action: 'approve_tools' }
|
|
@@ -20,6 +20,7 @@ import { ToolExecutor } from './executor.js'
|
|
|
20
20
|
export type EmitEvent = (event: RunEvent) => Promise<void>
|
|
21
21
|
|
|
22
22
|
export interface ToolingBootstrapConfig {
|
|
23
|
+
fileReadTracker?: import('../../types/tool/index.js').FileReadTracker
|
|
23
24
|
tools: ToolRegistryContract
|
|
24
25
|
runId: RunId
|
|
25
26
|
workingDirectory: string
|
|
@@ -67,6 +68,7 @@ export class ToolingBootstrap {
|
|
|
67
68
|
return new ToolExecutor(
|
|
68
69
|
{
|
|
69
70
|
tools: config.tools,
|
|
71
|
+
...(config.fileReadTracker ? { fileReadTracker: config.fileReadTracker } : {}),
|
|
70
72
|
runId: config.runId,
|
|
71
73
|
workingDirectory: config.workingDirectory,
|
|
72
74
|
...(config.additionalDirectories?.length
|
|
@@ -51,6 +51,14 @@ async function run() {
|
|
|
51
51
|
try { canonicalRoot = await fs.realpath(plan.root); }
|
|
52
52
|
catch (error) { if (missing(error)) return; throw error; }
|
|
53
53
|
if (canonicalRoot !== path.resolve(plan.root)) throw new Error('File walk root follows a symbolic link; use its authorized real directory');
|
|
54
|
+
const rootInfo = await fs.lstat(plan.root);
|
|
55
|
+
if (rootInfo.isFile()) {
|
|
56
|
+
visit(++visited);
|
|
57
|
+
if (matchers.some(matcher => matcher.test(path.basename(plan.root)))) {
|
|
58
|
+
await write({type:'entry', path:plan.root, size:rootInfo.size});
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
54
62
|
let start = plan.root;
|
|
55
63
|
for (const part of plan.prefix) {
|
|
56
64
|
start = path.join(start, part);
|