@llm4ts/core 2.40.0 → 2.41.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/dist/providers/ClaudeCliConnector.d.ts.map +1 -1
- package/dist/providers/ClaudeCliConnector.js +26 -13
- package/dist/providers/ClaudeCliConnector.js.map +1 -1
- package/dist/providers/CliSupport.d.ts +17 -1
- package/dist/providers/CliSupport.d.ts.map +1 -1
- package/dist/providers/CliSupport.js +40 -2
- package/dist/providers/CliSupport.js.map +1 -1
- package/dist/providers/CodexConnector.d.ts.map +1 -1
- package/dist/providers/CodexConnector.js +24 -5
- package/dist/providers/CodexConnector.js.map +1 -1
- package/dist/providers/GeminiCliProvider.d.ts.map +1 -1
- package/dist/providers/GeminiCliProvider.js +2 -16
- package/dist/providers/GeminiCliProvider.js.map +1 -1
- package/dist/providers/PiConnector.d.ts +10 -0
- package/dist/providers/PiConnector.d.ts.map +1 -1
- package/dist/providers/PiConnector.js +83 -12
- package/dist/providers/PiConnector.js.map +1 -1
- package/package.json +1 -1
- package/src/providers/ClaudeCliConnector.ts +37 -14
- package/src/providers/CliSupport.ts +63 -2
- package/src/providers/CodexConnector.ts +26 -3
- package/src/providers/GeminiCliProvider.ts +3 -17
- package/src/providers/PiConnector.ts +115 -27
|
@@ -2,10 +2,11 @@ import * as Effect from "effect/Effect"
|
|
|
2
2
|
import * as Stream from "effect/Stream"
|
|
3
3
|
import { makeCliConnector, type CliConnectorShape } from "../Connector.ts"
|
|
4
4
|
import type { CliConnectorConfig } from "../ConnectorConfig.ts"
|
|
5
|
-
import { ProviderError } from "../Errors.ts"
|
|
5
|
+
import { InvalidRequestError, ProviderError, type LlmError } from "../Errors.ts"
|
|
6
6
|
import { ConnectorCapabilities, ConnectorIds, LlmChunk, TokenUsage } from "../Models.ts"
|
|
7
7
|
import type { ProcessExecutorShape } from "../ProcessExecutor.ts"
|
|
8
8
|
import {
|
|
9
|
+
atLeastVersion,
|
|
9
10
|
effortWord,
|
|
10
11
|
cumulativeUsage,
|
|
11
12
|
failClassifiedCliError,
|
|
@@ -19,9 +20,41 @@ import {
|
|
|
19
20
|
toolEventChunk,
|
|
20
21
|
toolResultChunk,
|
|
21
22
|
toolResultText,
|
|
22
|
-
usageEventChunk
|
|
23
|
+
usageEventChunk,
|
|
24
|
+
versionTriple,
|
|
25
|
+
statusChunk
|
|
23
26
|
} from "./CliSupport.ts"
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The first pi whose `--tools` allowlist can be kept free of MCP tools: from
|
|
30
|
+
* 1.0.4 `--tools read` keeps every MCP tool the user configured in
|
|
31
|
+
* `~/.pi/agent/mcp.json` unless `--no-mcp` (new in that release) drops them,
|
|
32
|
+
* and MCP tools are built in since 0.99.0. Below this a read-only seat can
|
|
33
|
+
* reach any MCP write tool, so llm4ts refuses it (ADR 0010, ADR 0035).
|
|
34
|
+
*/
|
|
35
|
+
export const piReadOnlyFloor = "1.0.4"
|
|
36
|
+
|
|
37
|
+
/** Why this pi cannot hold a read-only seat, or nothing when it can. */
|
|
38
|
+
export const piReadOnlyProblem = (versionText: string): string | undefined => {
|
|
39
|
+
const version = versionTriple(versionText)
|
|
40
|
+
const floor = versionTriple(piReadOnlyFloor) ?? []
|
|
41
|
+
if (version === undefined) {
|
|
42
|
+
return (
|
|
43
|
+
`could not read the pi version from '${versionText.trim()}'; a read-only seat needs ` +
|
|
44
|
+
`pi >= ${piReadOnlyFloor}, whose --no-mcp keeps MCP tools out of the read allowlist`
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
return atLeastVersion(version, floor)
|
|
48
|
+
? undefined
|
|
49
|
+
: `pi ${version.join(".")} cannot enforce a read-only seat: --tools read keeps MCP tools ` +
|
|
50
|
+
`until --no-mcp arrived in ${piReadOnlyFloor}; upgrade pi, or give the judge and ` +
|
|
51
|
+
"reviewer seats to another harness"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The provider half of pi's `provider/model[:thinking]` model id. */
|
|
55
|
+
const providerOf = (model: string | undefined): string | undefined =>
|
|
56
|
+
model === undefined ? undefined : model.split("/", 1)[0]
|
|
57
|
+
|
|
25
58
|
export const piExtraArgs = (config: CliConnectorConfig): ReadonlyArray<string> => [
|
|
26
59
|
...optionalModelArgs(config.model),
|
|
27
60
|
// `noTools` takes precedence over `readOnly`: `--no-tools` disables tool
|
|
@@ -29,12 +62,22 @@ export const piExtraArgs = (config: CliConnectorConfig): ReadonlyArray<string> =
|
|
|
29
62
|
// offers a read tool the model can invoke — fine for an interactive
|
|
30
63
|
// session, unsafe for a one-shot complete() call with no tool-loop
|
|
31
64
|
// continuation (see the field's doc comment on CliConnectorConfig).
|
|
32
|
-
|
|
65
|
+
// `--no-tools` also drops MCP tools; `--tools read` keeps them (pi >= 1.0.4),
|
|
66
|
+
// so a read-only seat adds `--no-mcp` (ADR 0035).
|
|
67
|
+
...(config.noTools ? ["--no-tools"] : config.readOnly ? ["--tools", "read", "--no-mcp"] : []),
|
|
33
68
|
// `--thinking` is pi's effort flag (off, minimal, low, medium, high, xhigh).
|
|
34
69
|
...(config.effort === undefined ? [] : ["--thinking", effortWord(config.effort, "xhigh")]),
|
|
35
70
|
// Isolation is partial (ADR 0029): project extensions and skills stay
|
|
36
71
|
// out; pi still reads AGENTS.md from the working directory and its parents.
|
|
37
|
-
|
|
72
|
+
// `--no-extensions` also drops the built-in llama.cpp provider (pi >=
|
|
73
|
+
// 0.99.0), so a local model on it is re-enabled by name.
|
|
74
|
+
...(config.isolated
|
|
75
|
+
? [
|
|
76
|
+
"--no-extensions",
|
|
77
|
+
"--no-skills",
|
|
78
|
+
...(providerOf(config.model) === "llama.cpp" ? ["-e", "builtin:llama.cpp"] : [])
|
|
79
|
+
]
|
|
80
|
+
: []),
|
|
38
81
|
...sortedFlagArgs(config.flags)
|
|
39
82
|
]
|
|
40
83
|
|
|
@@ -53,27 +96,40 @@ export const parsePiStreamLine = (line: string): ReadonlyArray<LlmChunk> => {
|
|
|
53
96
|
: undefined
|
|
54
97
|
return delta === undefined || delta.length === 0 ? [] : [LlmChunk.make({ delta })]
|
|
55
98
|
}
|
|
56
|
-
|
|
99
|
+
// A call nested in another (codemode) names its parent (ADR 0033).
|
|
100
|
+
case "tool_execution_start": {
|
|
101
|
+
const parent = jsonStringField(json, "parentToolCallId")
|
|
57
102
|
return [
|
|
58
103
|
toolEventChunk(
|
|
59
104
|
jsonStringField(json, "toolName") ?? "",
|
|
60
105
|
jsonField(json, "args"),
|
|
61
|
-
jsonStringField(json, "toolCallId")
|
|
106
|
+
jsonStringField(json, "toolCallId"),
|
|
107
|
+
parent === undefined ? {} : { parent }
|
|
62
108
|
)
|
|
63
109
|
]
|
|
64
|
-
|
|
110
|
+
}
|
|
111
|
+
case "tool_execution_end": {
|
|
112
|
+
const parent = jsonStringField(json, "parentToolCallId")
|
|
113
|
+
const durationMs = jsonIntField(json, "durationMs")
|
|
65
114
|
return [
|
|
66
115
|
toolResultChunk(jsonStringField(json, "toolCallId"), {
|
|
67
116
|
failed: jsonField(json, "isError") === true,
|
|
117
|
+
...(parent === undefined ? {} : { parent }),
|
|
118
|
+
...(durationMs === undefined ? {} : { durationMs }),
|
|
68
119
|
...(() => {
|
|
69
120
|
const output = toolResultText(jsonField(jsonField(json, "result"), "content"))
|
|
70
121
|
return output === undefined ? {} : { output }
|
|
71
122
|
})()
|
|
72
123
|
})
|
|
73
124
|
]
|
|
125
|
+
}
|
|
74
126
|
case "message_end":
|
|
75
127
|
case "agent_end": {
|
|
76
128
|
const message = jsonField(json, "message")
|
|
129
|
+
// An aborted turn (a signal, a steer) is not a reply either.
|
|
130
|
+
if (jsonStringField(message, "stopReason") === "aborted") {
|
|
131
|
+
return [LlmChunk.make({ delta: "", metadata: { piError: "pi aborted the turn" } })]
|
|
132
|
+
}
|
|
77
133
|
// pi reports a provider refusal (a usage limit, an auth failure) as an
|
|
78
134
|
// assistant message that stopped with an error and exits 0; without
|
|
79
135
|
// this it reads as an empty, successful reply.
|
|
@@ -106,6 +162,10 @@ export const parsePiStreamLine = (line: string): ReadonlyArray<LlmChunk> => {
|
|
|
106
162
|
)
|
|
107
163
|
]
|
|
108
164
|
}
|
|
165
|
+
// A provider retry and a context compaction pause the turn: the lane
|
|
166
|
+
// says so instead of looking stuck (ADR 0033).
|
|
167
|
+
case "auto_retry_start":
|
|
168
|
+
return [statusChunk("retrying", "start", jsonStringField(json, "errorMessage"))]
|
|
109
169
|
case "auto_retry_end":
|
|
110
170
|
return jsonBooleanField(json, "success") === false
|
|
111
171
|
? [
|
|
@@ -116,7 +176,11 @@ export const parsePiStreamLine = (line: string): ReadonlyArray<LlmChunk> => {
|
|
|
116
176
|
}
|
|
117
177
|
})
|
|
118
178
|
]
|
|
119
|
-
: []
|
|
179
|
+
: [statusChunk("retrying", "end")]
|
|
180
|
+
case "compaction_start":
|
|
181
|
+
return [statusChunk("compacting", "start")]
|
|
182
|
+
case "compaction_end":
|
|
183
|
+
return [statusChunk("compacting", "end")]
|
|
120
184
|
case "extension_error":
|
|
121
185
|
return [
|
|
122
186
|
LlmChunk.make({
|
|
@@ -138,9 +202,27 @@ export const makePiConnector = (
|
|
|
138
202
|
const cwd = config.workingDir ?? "."
|
|
139
203
|
const extraArgs = piExtraArgs(config)
|
|
140
204
|
|
|
205
|
+
// A read-only seat is only taken on a pi whose read allowlist can exclude
|
|
206
|
+
// MCP tools (`piReadOnlyFloor`): the version answer decides before the
|
|
207
|
+
// first turn, once per connector; a failed probe is retried on the next.
|
|
208
|
+
let floorChecked = !config.readOnly
|
|
209
|
+
const checkFloorOnce: Effect.Effect<void, LlmError> = Effect.suspend(() =>
|
|
210
|
+
floorChecked
|
|
211
|
+
? Effect.void
|
|
212
|
+
: Effect.flatMap(executor.run(["pi", "--version"], cwd, config.envVars), (result) => {
|
|
213
|
+
const problem = piReadOnlyProblem([...result.stdout, ...result.stderr].join("\n"))
|
|
214
|
+
return problem === undefined
|
|
215
|
+
? Effect.sync(() => {
|
|
216
|
+
floorChecked = true
|
|
217
|
+
})
|
|
218
|
+
: Effect.fail(InvalidRequestError.make({ message: problem }))
|
|
219
|
+
})
|
|
220
|
+
)
|
|
221
|
+
|
|
141
222
|
const complete = Effect.fn("@llm4ts/core/providers/PiConnector.complete")(function* (
|
|
142
223
|
prompt: string
|
|
143
224
|
) {
|
|
225
|
+
yield* checkFloorOnce
|
|
144
226
|
const result = yield* executor.runWithStdin(
|
|
145
227
|
["pi", "-p", ...extraArgs],
|
|
146
228
|
cwd,
|
|
@@ -161,24 +243,26 @@ export const makePiConnector = (
|
|
|
161
243
|
})
|
|
162
244
|
|
|
163
245
|
const completeStream = (prompt: string) =>
|
|
164
|
-
|
|
165
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
246
|
+
Stream.fromEffect(checkFloorOnce).pipe(
|
|
247
|
+
Stream.drain,
|
|
248
|
+
Stream.concat(
|
|
249
|
+
executor.runStreamingWithStdin(
|
|
250
|
+
["pi", "-p", "--mode", "json", ...extraArgs],
|
|
251
|
+
cwd,
|
|
252
|
+
config.envVars,
|
|
253
|
+
prompt
|
|
254
|
+
)
|
|
255
|
+
),
|
|
256
|
+
Stream.flatMap((line) => Stream.fromIterable(parsePiStreamLine(line))),
|
|
257
|
+
// pi reports usage per assistant message; a turn is all of them.
|
|
258
|
+
cumulativeUsage,
|
|
259
|
+
Stream.mapEffect((chunk) => {
|
|
260
|
+
const message = chunk.metadata.piError
|
|
261
|
+
return message === undefined
|
|
262
|
+
? Effect.succeed(chunk)
|
|
263
|
+
: failClassifiedCliError("pi", "pi error", message)
|
|
264
|
+
})
|
|
265
|
+
)
|
|
182
266
|
|
|
183
267
|
return makeCliConnector({
|
|
184
268
|
id: ConnectorIds.Pi,
|
|
@@ -186,13 +270,17 @@ export const makePiConnector = (
|
|
|
186
270
|
// `--tools read` is pi's documented comma-separated ALLOWLIST of tool
|
|
187
271
|
// names: only `read` is enabled, so bash/edit/write are absent — a real
|
|
188
272
|
// capability removal, the same mechanism class as claude's `--tools`.
|
|
273
|
+
// `--no-mcp` keeps MCP tools out of it, and `checkFloorOnce` refuses a
|
|
274
|
+
// read-only seat on a pi without that flag, so the grade holds.
|
|
189
275
|
capabilities: ConnectorCapabilities.make({
|
|
190
276
|
interactiveSessions: true,
|
|
191
277
|
readOnlyEnforcement: "enforced",
|
|
192
278
|
effort: "mapped",
|
|
193
279
|
isolatedHeadless: "partial"
|
|
194
280
|
}),
|
|
195
|
-
|
|
281
|
+
// `--` ends the options (pi >= 0.84.3): a prompt starting with a dash
|
|
282
|
+
// is a prompt, not a flag. Stdin paths need no separator.
|
|
283
|
+
buildArgv: (prompt, _context) => ["pi", "-p", ...extraArgs, "--", prompt],
|
|
196
284
|
buildInteractiveArgv: (_context) => ["pi", ...extraArgs],
|
|
197
285
|
complete,
|
|
198
286
|
completeStream,
|