@namzu/sdk 26.0.0 → 27.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 +123 -0
- package/dist/compaction/manager.d.ts +22 -0
- package/dist/compaction/manager.d.ts.map +1 -1
- package/dist/compaction/manager.js +31 -3
- package/dist/compaction/manager.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts +7 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +12 -1
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +1 -0
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +3 -0
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/prompt-adapter.d.ts +10 -4
- package/dist/connector/mcp/prompt-adapter.d.ts.map +1 -1
- package/dist/connector/mcp/prompt-adapter.js +10 -4
- package/dist/connector/mcp/prompt-adapter.js.map +1 -1
- package/dist/connector/mcp/server-stdio.d.ts +62 -0
- package/dist/connector/mcp/server-stdio.d.ts.map +1 -0
- package/dist/connector/mcp/server-stdio.js +121 -0
- package/dist/connector/mcp/server-stdio.js.map +1 -0
- package/dist/eval/experiment.d.ts +0 -6
- package/dist/eval/experiment.d.ts.map +1 -1
- package/dist/eval/experiment.js +28 -0
- package/dist/eval/experiment.js.map +1 -1
- package/dist/eval/index.d.ts +2 -0
- package/dist/eval/index.d.ts.map +1 -1
- package/dist/eval/index.js +1 -0
- package/dist/eval/index.js.map +1 -1
- package/dist/eval/types.d.ts +17 -0
- package/dist/eval/types.d.ts.map +1 -1
- package/dist/eval/uncertainty.d.ts +64 -0
- package/dist/eval/uncertainty.d.ts.map +1 -0
- package/dist/eval/uncertainty.js +92 -0
- package/dist/eval/uncertainty.js.map +1 -0
- 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/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +2 -1
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/session/workspace/git-worktree.d.ts +15 -0
- package/dist/session/workspace/git-worktree.d.ts.map +1 -1
- package/dist/session/workspace/git-worktree.js +55 -1
- package/dist/session/workspace/git-worktree.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +35 -0
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/coordinator/agent.d.ts.map +1 -1
- package/dist/tools/coordinator/agent.js +12 -0
- package/dist/tools/coordinator/agent.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +134 -6
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/trusted-read-only.d.ts +42 -0
- package/dist/tools/trusted-read-only.d.ts.map +1 -0
- package/dist/tools/trusted-read-only.js +51 -0
- package/dist/tools/trusted-read-only.js.map +1 -0
- package/dist/tools/untrusted-envelope.d.ts +22 -2
- package/dist/tools/untrusted-envelope.d.ts.map +1 -1
- package/dist/tools/untrusted-envelope.js +22 -2
- package/dist/tools/untrusted-envelope.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +23 -0
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +28 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/verification/rules.d.ts.map +1 -1
- package/dist/verification/rules.js +5 -1
- package/dist/verification/rules.js.map +1 -1
- package/package.json +1 -1
- package/src/compaction/manager.ts +39 -3
- package/src/connector/mcp/adapter.ts +11 -0
- package/src/connector/mcp/index.ts +3 -0
- package/src/connector/mcp/prompt-adapter.ts +10 -4
- package/src/connector/mcp/server-stdio.ts +137 -0
- package/src/eval/experiment.ts +30 -0
- package/src/eval/index.ts +2 -0
- package/src/eval/types.ts +17 -0
- package/src/eval/uncertainty.ts +124 -0
- package/src/public-runtime.ts +1 -0
- package/src/registry/tool/execute.ts +2 -1
- package/src/session/workspace/git-worktree.ts +55 -1
- package/src/tools/builtins/bash.ts +34 -0
- package/src/tools/coordinator/agent.ts +12 -0
- package/src/tools/coordinator/index.ts +144 -8
- package/src/tools/trusted-read-only.ts +52 -0
- package/src/tools/untrusted-envelope.ts +22 -2
- package/src/types/sandbox/index.ts +23 -0
- package/src/types/tool/index.ts +30 -0
- package/src/verification/rules.ts +5 -1
|
@@ -34,10 +34,16 @@ import type { MCPClient } from './client.js'
|
|
|
34
34
|
/**
|
|
35
35
|
* Marks where a remote party's words begin and end.
|
|
36
36
|
*
|
|
37
|
-
* A prompt is composed by a SERVER. Untrusted content arriving
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* A prompt is composed by a SERVER. Untrusted content arriving this way is
|
|
38
|
+
* the standard prompt-injection surface, and an unlabelled block reads
|
|
39
|
+
* exactly like the agent's own instructions — so this says whose words
|
|
40
|
+
* they are.
|
|
41
|
+
*
|
|
42
|
+
* Marking, not stopping. See `tools/untrusted-envelope.ts` for the
|
|
43
|
+
* measurement: delimiting reports near-zero attack success on a static
|
|
44
|
+
* benchmark and above 95% once the attacker adapts (arXiv:2510.09023).
|
|
45
|
+
* This paragraph used to call it "the mitigation that survives contact",
|
|
46
|
+
* which was the same overstatement in a second file.
|
|
41
47
|
*/
|
|
42
48
|
export function renderPromptMessages(
|
|
43
49
|
serverName: string,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { Readable, Writable } from 'node:stream'
|
|
2
|
+
|
|
3
|
+
import type { MCPJsonRpcMessage, MCPTransport } from '../../types/connector/mcp.js'
|
|
4
|
+
import { type Logger, getRootLogger } from '../../utils/logger.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The server half of stdio, so `MCPServer` has something to run on.
|
|
8
|
+
*
|
|
9
|
+
* `MCPServer` is a complete implementation — `initialize`, `tools/list`,
|
|
10
|
+
* `tools/call`, resource and prompt providers — and nothing anywhere
|
|
11
|
+
* constructed one, because every transport in this directory is the
|
|
12
|
+
* CLIENT side. `StdioTransport` spawns a child and talks to its streams;
|
|
13
|
+
* this reads the streams THIS process was given. Same interface, opposite
|
|
14
|
+
* end of the pipe.
|
|
15
|
+
*
|
|
16
|
+
* Stdio is the first transport rather than an afterthought: a client
|
|
17
|
+
* spawns the server as a child process, so there is no port, no bind
|
|
18
|
+
* address and no inbound authentication question to get wrong. The
|
|
19
|
+
* boundary is the process, and the process was started by the client.
|
|
20
|
+
*
|
|
21
|
+
* **stdout belongs to the protocol.** A single stray `console.log`
|
|
22
|
+
* anywhere in the process corrupts the message stream, and the symptom is
|
|
23
|
+
* a client that reports malformed JSON rather than anything naming the
|
|
24
|
+
* culprit. This repository's logger already writes to stderr, which is
|
|
25
|
+
* what makes this transport safe to add; keep it that way.
|
|
26
|
+
*/
|
|
27
|
+
export class ServerStdioTransport implements MCPTransport {
|
|
28
|
+
private readonly input: Readable
|
|
29
|
+
private readonly output: Writable
|
|
30
|
+
private buffer = ''
|
|
31
|
+
private connected = false
|
|
32
|
+
private readonly log: Logger
|
|
33
|
+
|
|
34
|
+
private messageHandler?: (message: MCPJsonRpcMessage) => void
|
|
35
|
+
private closeHandler?: () => void
|
|
36
|
+
private errorHandler?: (error: Error) => void
|
|
37
|
+
|
|
38
|
+
private readonly onData = (chunk: Buffer | string): void => this.consume(chunk)
|
|
39
|
+
private readonly onEnd = (): void => {
|
|
40
|
+
this.connected = false
|
|
41
|
+
this.closeHandler?.()
|
|
42
|
+
}
|
|
43
|
+
private readonly onStreamError = (err: Error): void => this.errorHandler?.(err)
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Streams are injected rather than read from `process` directly so a
|
|
47
|
+
* test can drive this without owning the process's own stdio — a test
|
|
48
|
+
* that replaced `process.stdin` would break every other test sharing
|
|
49
|
+
* the runner.
|
|
50
|
+
*/
|
|
51
|
+
constructor(streams?: { input?: Readable; output?: Writable }) {
|
|
52
|
+
this.input = streams?.input ?? process.stdin
|
|
53
|
+
this.output = streams?.output ?? process.stdout
|
|
54
|
+
this.log = getRootLogger().child({ component: 'ServerStdioTransport' })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async connect(): Promise<void> {
|
|
58
|
+
if (this.connected) return
|
|
59
|
+
this.input.setEncoding?.('utf8')
|
|
60
|
+
this.input.on('data', this.onData)
|
|
61
|
+
this.input.on('end', this.onEnd)
|
|
62
|
+
this.input.on('error', this.onStreamError)
|
|
63
|
+
this.connected = true
|
|
64
|
+
this.log.info('MCP stdio server listening on this process')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async close(): Promise<void> {
|
|
68
|
+
if (!this.connected) return
|
|
69
|
+
this.input.off('data', this.onData)
|
|
70
|
+
this.input.off('end', this.onEnd)
|
|
71
|
+
this.input.off('error', this.onStreamError)
|
|
72
|
+
this.connected = false
|
|
73
|
+
this.closeHandler?.()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async send(message: MCPJsonRpcMessage): Promise<void> {
|
|
77
|
+
// Newline-delimited JSON, one message per line. A message carrying a
|
|
78
|
+
// literal newline would split into two unparseable halves, so it is
|
|
79
|
+
// stripped by `JSON.stringify` escaping rather than by trusting the
|
|
80
|
+
// payload.
|
|
81
|
+
this.output.write(`${JSON.stringify(message)}\n`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onMessage(handler: (message: MCPJsonRpcMessage) => void): void {
|
|
85
|
+
this.messageHandler = handler
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
onClose(handler: () => void): void {
|
|
89
|
+
this.closeHandler = handler
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
onError(handler: (error: Error) => void): void {
|
|
93
|
+
this.errorHandler = handler
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
isConnected(): boolean {
|
|
97
|
+
return this.connected
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A chunk is not a message. Reads arrive at whatever size the pipe
|
|
102
|
+
* hands over, so one read can carry half a message, several messages,
|
|
103
|
+
* or the tail of one and the head of the next. Buffering until a
|
|
104
|
+
* newline is the whole protocol framing.
|
|
105
|
+
*/
|
|
106
|
+
private consume(chunk: Buffer | string): void {
|
|
107
|
+
this.buffer += typeof chunk === 'string' ? chunk : chunk.toString('utf8')
|
|
108
|
+
|
|
109
|
+
let newline = this.buffer.indexOf('\n')
|
|
110
|
+
while (newline !== -1) {
|
|
111
|
+
const line = this.buffer.slice(0, newline).trim()
|
|
112
|
+
this.buffer = this.buffer.slice(newline + 1)
|
|
113
|
+
if (line) this.deliver(line)
|
|
114
|
+
newline = this.buffer.indexOf('\n')
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private deliver(line: string): void {
|
|
119
|
+
let message: MCPJsonRpcMessage
|
|
120
|
+
try {
|
|
121
|
+
message = JSON.parse(line) as MCPJsonRpcMessage
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// Reported, not thrown, and not fatal. A client that sends one bad
|
|
124
|
+
// line has not ended the session, and killing the transport over it
|
|
125
|
+
// would take down every other conversation on this pipe. The
|
|
126
|
+
// alternative — silence — is worse: a request that vanishes looks
|
|
127
|
+
// to the client exactly like a server that hung.
|
|
128
|
+
this.errorHandler?.(
|
|
129
|
+
new Error(
|
|
130
|
+
`MCP stdio server could not parse a message: ${err instanceof Error ? err.message : String(err)}`,
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
this.messageHandler?.(message)
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/eval/experiment.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
Score,
|
|
8
8
|
Scorer,
|
|
9
9
|
} from './types.js'
|
|
10
|
+
import type { ScoreUncertainty } from './uncertainty.js'
|
|
11
|
+
import { describeUncertainty, uncertaintyOf } from './uncertainty.js'
|
|
10
12
|
|
|
11
13
|
export interface ExperimentConfig<TInput = unknown> {
|
|
12
14
|
name: string
|
|
@@ -151,6 +153,10 @@ export async function runExperiment<TInput>(
|
|
|
151
153
|
name: config.name,
|
|
152
154
|
cases: settled,
|
|
153
155
|
mean,
|
|
156
|
+
// Over the same cases the mean is over. Computing spread across a
|
|
157
|
+
// different denominator than the average it qualifies would produce
|
|
158
|
+
// an interval that does not belong to the number beside it.
|
|
159
|
+
uncertainty: uncertaintyOf(scored.map((r) => r.mean)),
|
|
154
160
|
passed: settled.filter((r) => r.status === 'passed').length,
|
|
155
161
|
failed: settled.filter((r) => r.status === 'failed').length,
|
|
156
162
|
inconclusive: settled.filter((r) => r.status === 'inconclusive').length,
|
|
@@ -278,9 +284,33 @@ function meanByScorer(results: readonly CaseResult[]): Record<string, number> {
|
|
|
278
284
|
* Failures print their scorer reasons, because a CI log that says
|
|
279
285
|
* "0.62" is a log that sends someone back to reproduce it by hand.
|
|
280
286
|
*/
|
|
287
|
+
/**
|
|
288
|
+
* Uncertainty for a report that did not carry its own.
|
|
289
|
+
*
|
|
290
|
+
* A suite file is loaded at runtime and may be plain JavaScript, so a
|
|
291
|
+
* report can reach here hand-built — the type cannot stop it. Deriving
|
|
292
|
+
* from the cases it does carry is better than either alternative:
|
|
293
|
+
* printing the mean alone leaves the reader where they started, and
|
|
294
|
+
* refusing to format would turn a missing convenience into a broken
|
|
295
|
+
* command.
|
|
296
|
+
*
|
|
297
|
+
* Uses the same exclusion `runExperiment` uses, so a derived interval and
|
|
298
|
+
* a carried one are the same number rather than two conventions.
|
|
299
|
+
*/
|
|
300
|
+
function derivedUncertainty(report: ExperimentReport): ScoreUncertainty {
|
|
301
|
+
return uncertaintyOf(report.cases.filter((c) => c.status !== 'inconclusive').map((c) => c.mean))
|
|
302
|
+
}
|
|
303
|
+
|
|
281
304
|
export function formatReport(report: ExperimentReport): string {
|
|
282
305
|
const lines: string[] = [
|
|
283
306
|
`${report.name}: ${report.passed}/${report.cases.length} passed (mean ${report.mean.toFixed(2)}) in ${report.durationMs}ms`,
|
|
307
|
+
// On its own line and always printed, including when the interval is
|
|
308
|
+
// undefined. A mean printed alone is the thing that has been
|
|
309
|
+
// over-read: two runs three points apart look like a difference, and
|
|
310
|
+
// at the n a hand-built suite has they are usually the same run
|
|
311
|
+
// twice. Computing the interval and not showing it would leave the
|
|
312
|
+
// reader exactly where they started.
|
|
313
|
+
` ${describeUncertainty(report.mean, report.uncertainty ?? derivedUncertainty(report))}`,
|
|
284
314
|
'',
|
|
285
315
|
]
|
|
286
316
|
|
package/src/eval/index.ts
CHANGED
package/src/eval/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StepResult } from '../types/run/step.js'
|
|
2
|
+
import type { ScoreUncertainty } from './uncertainty.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* One case in a dataset: an input, and what a good run looks like.
|
|
@@ -143,6 +144,22 @@ export interface ExperimentReport {
|
|
|
143
144
|
cases: readonly CaseResult[]
|
|
144
145
|
/** Mean score across the cases that produced one. */
|
|
145
146
|
mean: number
|
|
147
|
+
/**
|
|
148
|
+
* How much of {@link mean} is signal.
|
|
149
|
+
*
|
|
150
|
+
* A mean on its own has been read as a result, and at the n a
|
|
151
|
+
* hand-built suite has it usually is not one: two runs three points
|
|
152
|
+
* apart are normally the same run twice.
|
|
153
|
+
*
|
|
154
|
+
* Optional, and deliberately so after trying it the other way. A suite
|
|
155
|
+
* file is loaded at runtime and may be plain JavaScript, so a required
|
|
156
|
+
* field is not enforced at the boundary that matters — it buys type
|
|
157
|
+
* safety for one kind of consumer and a crash for the other. Producers
|
|
158
|
+
* that go through `runExperiment` always set it; `formatReport` derives
|
|
159
|
+
* it from {@link cases} when a hand-built report does not, so no report
|
|
160
|
+
* is printed without an interval either way.
|
|
161
|
+
*/
|
|
162
|
+
uncertainty?: ScoreUncertainty
|
|
146
163
|
passed: number
|
|
147
164
|
failed: number
|
|
148
165
|
/** Cases where no scorer could produce a judgement. */
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much of a score is signal.
|
|
3
|
+
*
|
|
4
|
+
* A suite reported a mean and nothing else, so two runs differing by three
|
|
5
|
+
* points read as a difference. At the n a hand-built suite has, that is
|
|
6
|
+
* usually noise, and there was no number on the page that would have said
|
|
7
|
+
* so.
|
|
8
|
+
*
|
|
9
|
+
* Evan Miller, "Adding Error Bars to Evals" (arXiv:2411.00640), is the
|
|
10
|
+
* reference. Two of its results shape what is and is not computed here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Two-sided 95% critical values of Student's t, by degrees of freedom.
|
|
15
|
+
*
|
|
16
|
+
* The normal approximation (1.96) is what most harnesses use and it is
|
|
17
|
+
* wrong in the direction that matters: at n=5 the true multiplier is 2.78,
|
|
18
|
+
* so a normal interval is nearly 30% too narrow exactly where a suite is
|
|
19
|
+
* small enough for that to mislead. Eval suites are small; this table is
|
|
20
|
+
* the difference between an interval that covers and one that flatters.
|
|
21
|
+
*/
|
|
22
|
+
const T_95: readonly number[] = [
|
|
23
|
+
12.706, 4.303, 3.182, 2.776, 2.571, 2.447, 2.365, 2.306, 2.262, 2.228, 2.201, 2.179, 2.16, 2.145,
|
|
24
|
+
2.131, 2.12, 2.11, 2.101, 2.093, 2.086, 2.08, 2.074, 2.069, 2.064, 2.06, 2.056, 2.052, 2.048,
|
|
25
|
+
2.045, 2.042,
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
function critical95(df: number): number {
|
|
29
|
+
if (df < 1) return Number.NaN
|
|
30
|
+
// Beyond 30 the t value is within ~1% of the normal, and pretending
|
|
31
|
+
// otherwise would imply a precision the rest of this does not have.
|
|
32
|
+
return T_95[df - 1] ?? 1.96
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ScoreUncertainty {
|
|
36
|
+
/** Cases that produced a score. Not the number of cases run. */
|
|
37
|
+
readonly n: number
|
|
38
|
+
/** Sample standard deviation, Bessel-corrected. */
|
|
39
|
+
readonly stdDev: number
|
|
40
|
+
/** Standard error of the mean. */
|
|
41
|
+
readonly stdError: number
|
|
42
|
+
/** Half-width of the 95% interval: the mean plus or minus this. */
|
|
43
|
+
readonly margin95: number
|
|
44
|
+
/**
|
|
45
|
+
* The 95% interval, clamped to the score range.
|
|
46
|
+
*
|
|
47
|
+
* Clamped because a mean of 0.95 with a wide interval otherwise reports
|
|
48
|
+
* an upper bound above 1, which is not a possible score and makes a
|
|
49
|
+
* reader distrust the whole figure. The clamp is cosmetic and the
|
|
50
|
+
* margin above is not — read that one for the width.
|
|
51
|
+
*/
|
|
52
|
+
readonly ci95: readonly [low: number, high: number]
|
|
53
|
+
/**
|
|
54
|
+
* True when there is not enough data for an interval at all.
|
|
55
|
+
*
|
|
56
|
+
* One case has no spread to measure. Reporting `±0` there would be the
|
|
57
|
+
* most confident-looking output the suite can produce, from the least
|
|
58
|
+
* evidence it can have.
|
|
59
|
+
*/
|
|
60
|
+
readonly undefinedInterval: boolean
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Spread of a set of scores, with the interval a reader should apply.
|
|
65
|
+
*
|
|
66
|
+
* **Assumes the cases are independent, and they may not be.** Miller's
|
|
67
|
+
* clustered standard errors run up to 3x the naive figure when cases come
|
|
68
|
+
* in related groups — several cases derived from one scenario, or one
|
|
69
|
+
* document, or one seed. This harness has no grouping key on a case, so
|
|
70
|
+
* there is nothing here to cluster on and this returns the naive figure.
|
|
71
|
+
* Where a suite does build several cases from one source, treat the
|
|
72
|
+
* interval below as a floor rather than as the answer.
|
|
73
|
+
*
|
|
74
|
+
* Stated rather than silently assumed because a too-narrow interval is
|
|
75
|
+
* worse than none: it turns "we cannot tell" into a number that looks
|
|
76
|
+
* like we can.
|
|
77
|
+
*/
|
|
78
|
+
export function uncertaintyOf(scores: readonly number[]): ScoreUncertainty {
|
|
79
|
+
const n = scores.length
|
|
80
|
+
if (n < 2) {
|
|
81
|
+
return {
|
|
82
|
+
n,
|
|
83
|
+
stdDev: 0,
|
|
84
|
+
stdError: 0,
|
|
85
|
+
margin95: Number.NaN,
|
|
86
|
+
ci95: [Number.NaN, Number.NaN],
|
|
87
|
+
undefinedInterval: true,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const mean = scores.reduce((a, b) => a + b, 0) / n
|
|
92
|
+
// Bessel-corrected: dividing by n estimates the spread of THESE cases,
|
|
93
|
+
// and the question is about the suite they were drawn from.
|
|
94
|
+
const variance = scores.reduce((acc, s) => acc + (s - mean) ** 2, 0) / (n - 1)
|
|
95
|
+
const stdDev = Math.sqrt(variance)
|
|
96
|
+
const stdError = stdDev / Math.sqrt(n)
|
|
97
|
+
const margin95 = critical95(n - 1) * stdError
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
n,
|
|
101
|
+
stdDev,
|
|
102
|
+
stdError,
|
|
103
|
+
margin95,
|
|
104
|
+
ci95: [Math.max(0, mean - margin95), Math.min(1, mean + margin95)],
|
|
105
|
+
undefinedInterval: false,
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One line a reader can act on, for a surface that prints a score.
|
|
111
|
+
*
|
|
112
|
+
* Names the interval rather than only the mean, because the mean alone is
|
|
113
|
+
* the thing that has been over-read. An interval spanning most of the
|
|
114
|
+
* scale says the suite cannot currently tell two runs apart, and that is
|
|
115
|
+
* the most useful sentence such a suite can produce.
|
|
116
|
+
*/
|
|
117
|
+
export function describeUncertainty(mean: number, u: ScoreUncertainty): string {
|
|
118
|
+
if (u.undefinedInterval) {
|
|
119
|
+
return u.n === 0
|
|
120
|
+
? 'no scored cases, so no score'
|
|
121
|
+
: `${mean.toFixed(3)} from a single case — no interval, and one case cannot show spread`
|
|
122
|
+
}
|
|
123
|
+
return `${mean.toFixed(3)} ±${u.margin95.toFixed(3)} (95% CI ${u.ci95[0].toFixed(3)}–${u.ci95[1].toFixed(3)}, n=${u.n}); assumes cases are independent`
|
|
124
|
+
}
|
package/src/public-runtime.ts
CHANGED
|
@@ -63,6 +63,7 @@ export {
|
|
|
63
63
|
export { normaliseModelId, resolveModelPricing, VENDOR_RATES } from './pricing/index.js'
|
|
64
64
|
export { toErrorMessage } from './utils/error.js'
|
|
65
65
|
export { configureLogger, getRootLogger, Logger } from './utils/logger.js'
|
|
66
|
+
export { isTrustedReadOnly } from './tools/trusted-read-only.js'
|
|
66
67
|
export { buildToolResultHashes, hashToolResult } from './utils/hash.js'
|
|
67
68
|
export {
|
|
68
69
|
compressShellOutput,
|
|
@@ -3,6 +3,7 @@ import { assertStrictSchema } from '../../provider/strict-schema.js'
|
|
|
3
3
|
import { GENAI, NAMZU, toolSpanName } from '../../telemetry/attributes.js'
|
|
4
4
|
import { recordToolCall } from '../../telemetry/metrics.js'
|
|
5
5
|
import { getTracer } from '../../telemetry/runtime-accessors.js'
|
|
6
|
+
import { isTrustedReadOnly } from '../../tools/trusted-read-only.js'
|
|
6
7
|
import type {
|
|
7
8
|
LLMToolSchema,
|
|
8
9
|
ToolAvailability,
|
|
@@ -487,7 +488,7 @@ Executable tool names, descriptions, and JSON input schemas are attached through
|
|
|
487
488
|
|
|
488
489
|
const mode = context.permissionContext?.mode ?? 'auto'
|
|
489
490
|
if (mode === 'plan') {
|
|
490
|
-
const isReadOnly = tool
|
|
491
|
+
const isReadOnly = isTrustedReadOnly(tool, rawInput)
|
|
491
492
|
if (!isReadOnly) {
|
|
492
493
|
const msg = `plan mode: non-read-only tool "${toolName}" blocked`
|
|
493
494
|
span.setAttributes({
|
|
@@ -89,7 +89,26 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
89
89
|
try {
|
|
90
90
|
await this.exec('git', argv)
|
|
91
91
|
} catch (cause) {
|
|
92
|
-
|
|
92
|
+
// A non-zero exit here does not mean the worktree was not created.
|
|
93
|
+
// `git worktree add` runs the repository's post-checkout hook AFTER
|
|
94
|
+
// the checkout has completed, so a hook that fails — or that a
|
|
95
|
+
// timeout kills — reports failure over a worktree that is finished
|
|
96
|
+
// and usable. Treating the status as the answer throws away a good
|
|
97
|
+
// checkout AND leaks it: the path stays registered, and the next
|
|
98
|
+
// attempt fails differently, with "already exists".
|
|
99
|
+
//
|
|
100
|
+
// So the exit code is a hint and the repository is the evidence.
|
|
101
|
+
// The bar is deliberately high: registered under this exact path
|
|
102
|
+
// AND carrying the branch this call asked for. A registered path
|
|
103
|
+
// alone can be a half-finished checkout, or somebody else's.
|
|
104
|
+
if (!(await this.createdDespite(worktreePath, branch))) {
|
|
105
|
+
throw new WorkspaceBackendError({ op: 'create', kind: this.kind, cause })
|
|
106
|
+
}
|
|
107
|
+
this.log.warn('git-worktree add reported failure but the worktree is present', {
|
|
108
|
+
branch,
|
|
109
|
+
worktreePath,
|
|
110
|
+
cause: cause instanceof Error ? cause.message : String(cause),
|
|
111
|
+
})
|
|
93
112
|
}
|
|
94
113
|
|
|
95
114
|
const meta: GitWorktreeBackendMeta = {
|
|
@@ -142,6 +161,41 @@ export class GitWorktreeDriver implements WorkspaceBackendDriver {
|
|
|
142
161
|
}
|
|
143
162
|
}
|
|
144
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Did the worktree arrive despite the command reporting failure?
|
|
166
|
+
*
|
|
167
|
+
* Answers only for the branch this call created. A path registered
|
|
168
|
+
* without that branch is not this call's worktree — it is a leftover
|
|
169
|
+
* from a killed attempt, or a checkout somebody else owns, and the two
|
|
170
|
+
* are indistinguishable from here. Claiming either would mean handing
|
|
171
|
+
* a caller a workspace whose contents nobody vouched for, so both are
|
|
172
|
+
* left to surface as the failure they are.
|
|
173
|
+
*
|
|
174
|
+
* Any error while checking is itself a "no". This runs on a path that
|
|
175
|
+
* has already gone wrong once, and guessing optimistically there is how
|
|
176
|
+
* a recovery turns a bad situation into a wrong one.
|
|
177
|
+
*/
|
|
178
|
+
private async createdDespite(worktreePath: string, branch: string): Promise<boolean> {
|
|
179
|
+
try {
|
|
180
|
+
const { stdout } = await this.exec('git', [
|
|
181
|
+
'-C',
|
|
182
|
+
this.repoRoot,
|
|
183
|
+
'worktree',
|
|
184
|
+
'list',
|
|
185
|
+
'--porcelain',
|
|
186
|
+
])
|
|
187
|
+
const entry = parseWorktreeList(stdout, worktreePath)
|
|
188
|
+
// `--porcelain` writes the branch as a full ref (`refs/heads/x`),
|
|
189
|
+
// and `branch` here is the short name this call passed to `-b`.
|
|
190
|
+
// Comparing them directly is a check that can never pass, which
|
|
191
|
+
// would make this whole recovery path silently dead — the exact
|
|
192
|
+
// shape it exists to catch.
|
|
193
|
+
return entry?.branch === `refs/heads/${branch}`
|
|
194
|
+
} catch {
|
|
195
|
+
return false
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
145
199
|
async inspect(ref: WorkspaceRef): Promise<WorkspaceInspection> {
|
|
146
200
|
let listStdout: string
|
|
147
201
|
try {
|
|
@@ -60,6 +60,27 @@ const inputSchema = z.object({
|
|
|
60
60
|
|
|
61
61
|
type BashInput = z.infer<typeof inputSchema>
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The last line worth showing from one chunk of streamed output.
|
|
65
|
+
*
|
|
66
|
+
* A progress line is a status, not a log: the host renders one line and
|
|
67
|
+
* replaces it as the next arrives, so sending a whole chunk sends a wall
|
|
68
|
+
* of text into a slot that shows one line of it. A chunk usually ends
|
|
69
|
+
* mid-line and usually ends with a newline, so the last NON-EMPTY line is
|
|
70
|
+
* the most recent complete thing the command actually said.
|
|
71
|
+
*
|
|
72
|
+
* Progress is capped rather than truncated with an ellipsis: this is
|
|
73
|
+
* glanced at, and a marker in a line nobody reads to the end is noise.
|
|
74
|
+
*/
|
|
75
|
+
function lastNonEmptyLine(chunk: string): string | undefined {
|
|
76
|
+
const lines = chunk.split('\n')
|
|
77
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
78
|
+
const line = lines[i]?.trim()
|
|
79
|
+
if (line) return line.length > 160 ? line.slice(0, 160) : line
|
|
80
|
+
}
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
function isDangerousCommand(command: string): boolean {
|
|
64
85
|
return DANGEROUS_PATTERNS.some((pattern) => pattern.test(command))
|
|
65
86
|
}
|
|
@@ -114,6 +135,19 @@ export const BashTool = defineTool({
|
|
|
114
135
|
// Same reason as the host path below: a Stop must reach the
|
|
115
136
|
// process, not just the promise waiting on it.
|
|
116
137
|
signal: context.abortSignal,
|
|
138
|
+
// The worker has always streamed its output; nothing asked for
|
|
139
|
+
// it, so a command that ran for minutes said nothing until it
|
|
140
|
+
// exited. `report` is ephemeral by design — it answers "is it
|
|
141
|
+
// still working?" for a live view and is excluded from the
|
|
142
|
+
// durable transcript — so this is a progress signal, not a
|
|
143
|
+
// second copy of the output. `result.stdout` remains the
|
|
144
|
+
// answer the model is given.
|
|
145
|
+
onOutput: context.report
|
|
146
|
+
? ({ data }) => {
|
|
147
|
+
const line = lastNonEmptyLine(data)
|
|
148
|
+
if (line) context.report?.(line)
|
|
149
|
+
}
|
|
150
|
+
: undefined,
|
|
117
151
|
})
|
|
118
152
|
|
|
119
153
|
if (result.timedOut) {
|
|
@@ -7,6 +7,7 @@ import { defineTool } from '../defineTool.js'
|
|
|
7
7
|
import { wrapUntrusted } from '../untrusted-envelope.js'
|
|
8
8
|
import { failureLabel, taskSucceeded } from './outcome.js'
|
|
9
9
|
|
|
10
|
+
import { DELEGATION_TIMEOUT_MS } from './index.js'
|
|
10
11
|
import type { TaskLaunchedCallback } from './index.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
@@ -107,6 +108,17 @@ export function buildAgentTool(opts: AgentToolOptions): ToolDefinition {
|
|
|
107
108
|
readOnly: false,
|
|
108
109
|
destructive: false,
|
|
109
110
|
concurrencySafe: true,
|
|
111
|
+
// Declaring nothing here does not mean "no deadline"; it means the
|
|
112
|
+
// executor's 120-second default, which is a bound for a tool call and
|
|
113
|
+
// absurd for a whole agent run. `create_task` in the sibling module
|
|
114
|
+
// carries the same reasoning and the same hour, and the measurement
|
|
115
|
+
// behind that number is in its docblock: three delegated children took
|
|
116
|
+
// 4m21s, 5m58s and 8m04s, and all three parents gave up at 120s.
|
|
117
|
+
//
|
|
118
|
+
// This surface did not get that fix when its twin did, and the file's
|
|
119
|
+
// own note above records the pair doing exactly this before. The two
|
|
120
|
+
// tools are twins; a bound applied to one of them is not applied.
|
|
121
|
+
timeoutMs: DELEGATION_TIMEOUT_MS,
|
|
110
122
|
...(opts.terminal !== undefined ? { terminal: opts.terminal } : {}),
|
|
111
123
|
async execute({ description, prompt, subagent_type }, context) {
|
|
112
124
|
// With a single registered subagent the type is optional — default to
|