@llm4ts/core 2.35.4 → 2.36.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.
Files changed (52) hide show
  1. package/dist/ConnectorConfig.d.ts +12 -0
  2. package/dist/ConnectorConfig.d.ts.map +1 -1
  3. package/dist/ConnectorConfig.js +18 -4
  4. package/dist/ConnectorConfig.js.map +1 -1
  5. package/dist/Models.d.ts +29 -0
  6. package/dist/Models.d.ts.map +1 -1
  7. package/dist/Models.js +28 -2
  8. package/dist/Models.js.map +1 -1
  9. package/dist/providers/AnthropicModels.d.ts +36 -3
  10. package/dist/providers/AnthropicModels.d.ts.map +1 -1
  11. package/dist/providers/AnthropicModels.js +35 -6
  12. package/dist/providers/AnthropicModels.js.map +1 -1
  13. package/dist/providers/AnthropicProvider.d.ts +18 -2
  14. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  15. package/dist/providers/AnthropicProvider.js +89 -20
  16. package/dist/providers/AnthropicProvider.js.map +1 -1
  17. package/dist/providers/ClaudeCliConnector.d.ts.map +1 -1
  18. package/dist/providers/ClaudeCliConnector.js +14 -3
  19. package/dist/providers/ClaudeCliConnector.js.map +1 -1
  20. package/dist/providers/CliSupport.d.ts +6 -1
  21. package/dist/providers/CliSupport.d.ts.map +1 -1
  22. package/dist/providers/CliSupport.js +5 -0
  23. package/dist/providers/CliSupport.js.map +1 -1
  24. package/dist/providers/CodexConnector.d.ts.map +1 -1
  25. package/dist/providers/CodexConnector.js +17 -3
  26. package/dist/providers/CodexConnector.js.map +1 -1
  27. package/dist/providers/GeminiCliProvider.d.ts +2 -0
  28. package/dist/providers/GeminiCliProvider.d.ts.map +1 -1
  29. package/dist/providers/GeminiCliProvider.js +10 -2
  30. package/dist/providers/GeminiCliProvider.js.map +1 -1
  31. package/dist/providers/OpenAIModels.d.ts +2 -0
  32. package/dist/providers/OpenAIModels.d.ts.map +1 -1
  33. package/dist/providers/OpenAIModels.js +2 -0
  34. package/dist/providers/OpenAIModels.js.map +1 -1
  35. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  36. package/dist/providers/OpenAIProvider.js +6 -3
  37. package/dist/providers/OpenAIProvider.js.map +1 -1
  38. package/dist/providers/PiConnector.d.ts.map +1 -1
  39. package/dist/providers/PiConnector.js +9 -2
  40. package/dist/providers/PiConnector.js.map +1 -1
  41. package/package.json +1 -1
  42. package/src/ConnectorConfig.ts +18 -4
  43. package/src/Models.ts +37 -1
  44. package/src/providers/AnthropicModels.ts +41 -6
  45. package/src/providers/AnthropicProvider.ts +136 -39
  46. package/src/providers/ClaudeCliConnector.ts +15 -3
  47. package/src/providers/CliSupport.ts +8 -1
  48. package/src/providers/CodexConnector.ts +17 -2
  49. package/src/providers/GeminiCliProvider.ts +10 -2
  50. package/src/providers/OpenAIModels.ts +2 -0
  51. package/src/providers/OpenAIProvider.ts +6 -2
  52. package/src/providers/PiConnector.ts +9 -1
@@ -1,5 +1,6 @@
1
1
  import * as Effect from "effect/Effect"
2
2
  import * as Redacted from "effect/Redacted"
3
+ import * as Ref from "effect/Ref"
3
4
  import * as Schema from "effect/Schema"
4
5
  import * as Stream from "effect/Stream"
5
6
  import { makeApiConnector, type ApiConnectorShape } from "../Connector.ts"
@@ -7,8 +8,10 @@ import { AuthenticationError, ConfigError, ParseError, type LlmError } from "../
7
8
  import type { HttpClientShape } from "../HttpClient.ts"
8
9
  import type { StructuredResult } from "../LlmService.ts"
9
10
  import {
11
+ ConnectorCapabilities,
10
12
  ConnectorIds,
11
13
  LlmChunk,
14
+ TokenUsage,
12
15
  ToolCall,
13
16
  ToolCallResponse,
14
17
  type JsonSchema,
@@ -20,14 +23,18 @@ import { collect } from "../Streaming.ts"
20
23
  import { parseFromText } from "../StructuredOutput.ts"
21
24
  import type { AnthropicContentBlockFull } from "./AnthropicModels.ts"
22
25
  import {
26
+ AnthropicCacheControl,
23
27
  AnthropicMessage,
28
+ AnthropicOutputConfig,
24
29
  AnthropicRequest,
25
30
  AnthropicRequestWithTools,
26
31
  AnthropicResponse,
27
32
  AnthropicResponseWithTools,
28
33
  AnthropicStreamChunk,
34
+ AnthropicTextBlock,
29
35
  AnthropicTool,
30
- AnthropicToolInputSchema
36
+ AnthropicToolInputSchema,
37
+ AnthropicUsage
31
38
  } from "./AnthropicModels.ts"
32
39
 
33
40
  interface AnthropicRequiredConfig {
@@ -84,20 +91,76 @@ export const anthropicHistory = (messages: ReadonlyArray<Message>): AnthropicHis
84
91
  )
85
92
  })
86
93
 
94
+ const ephemeral = AnthropicCacheControl.make({ type: "ephemeral" })
95
+
96
+ const cachedBlock = (text: string): AnthropicTextBlock =>
97
+ AnthropicTextBlock.make({ type: "text", text, cache_control: ephemeral })
98
+
99
+ /**
100
+ * Prompt-cache breakpoints (ADR 0029): one on the system block, one on the
101
+ * last message. The API caches the prefix up to each marker and looks the
102
+ * next request up at earlier markers too, so a chat whose history only
103
+ * grows at the end reads every earlier turn from the cache.
104
+ */
105
+ export const withCacheMarkers = (
106
+ messages: ReadonlyArray<AnthropicMessage>,
107
+ system: string | undefined
108
+ ): {
109
+ readonly messages: ReadonlyArray<AnthropicMessage>
110
+ readonly system: string | ReadonlyArray<AnthropicTextBlock> | undefined
111
+ } => {
112
+ const last = messages[messages.length - 1]
113
+ const marked =
114
+ last === undefined || typeof last.content !== "string"
115
+ ? messages
116
+ : [
117
+ ...messages.slice(0, -1),
118
+ AnthropicMessage.make({ role: last.role, content: [cachedBlock(last.content)] })
119
+ ]
120
+ return {
121
+ messages: marked,
122
+ system: system === undefined || system.length === 0 ? system : [cachedBlock(system)]
123
+ }
124
+ }
125
+
87
126
  export const buildAnthropicRequest = (
88
127
  config: LlmConfig,
89
128
  messages: ReadonlyArray<AnthropicMessage>,
90
129
  stream: boolean,
91
130
  system?: string
92
- ): AnthropicRequest =>
93
- AnthropicRequest.make({
131
+ ): AnthropicRequest => {
132
+ const cached =
133
+ config.promptCache === false ? { messages, system } : withCacheMarkers(messages, system)
134
+ return AnthropicRequest.make({
94
135
  model: config.model,
95
136
  max_tokens: config.maxTokens ?? 4096,
96
- messages,
137
+ messages: cached.messages,
97
138
  stream,
98
139
  ...(config.temperature === undefined ? {} : { temperature: config.temperature }),
99
- ...(system === undefined ? {} : { system })
140
+ ...(cached.system === undefined ? {} : { system: cached.system }),
141
+ ...(config.effort === undefined
142
+ ? {}
143
+ : { output_config: AnthropicOutputConfig.make({ effort: config.effort }) })
144
+ })
145
+ }
146
+
147
+ /**
148
+ * The API's usage as llm4ts counts it: cache reads and writes are prompt
149
+ * tokens the request paid for (at their own rates), `cached` is what was
150
+ * read from the cache.
151
+ */
152
+ export const anthropicTokenUsage = (usage: AnthropicUsage): TokenUsage => {
153
+ const read = usage.cache_read_input_tokens ?? 0
154
+ const written = usage.cache_creation_input_tokens ?? 0
155
+ const prompt = (usage.input_tokens ?? 0) + read + written
156
+ const completion = usage.output_tokens ?? 0
157
+ return TokenUsage.make({
158
+ prompt,
159
+ completion,
160
+ total: prompt + completion,
161
+ ...(read > 0 ? { cached: read } : {})
100
162
  })
163
+ }
101
164
 
102
165
  export const buildAnthropicToolRequest = (
103
166
  config: LlmConfig,
@@ -123,7 +186,10 @@ export const buildAnthropicToolRequest = (
123
186
  })
124
187
  })
125
188
  ),
126
- ...(config.temperature === undefined ? {} : { temperature: config.temperature })
189
+ ...(config.temperature === undefined ? {} : { temperature: config.temperature }),
190
+ ...(config.effort === undefined
191
+ ? {}
192
+ : { output_config: AnthropicOutputConfig.make({ effort: config.effort }) })
127
193
  })
128
194
 
129
195
  const contentFromResponse = (
@@ -195,46 +261,76 @@ export const makeAnthropicProvider = (
195
261
  Stream.unwrap(
196
262
  Effect.map(requiredConfig, ({ apiKey, baseUrl }) => {
197
263
  const request = buildAnthropicRequest(config, messages, true, system)
198
- return httpClient
199
- .postJsonStreamSSE(
200
- `${baseUrl}/messages`,
201
- JSON.stringify(request),
202
- authHeaders(apiKey),
203
- config.timeout
204
- )
205
- .pipe(
206
- Stream.mapEffect(decodeStreamChunk),
207
- Stream.flatMap((chunk) => {
208
- if (chunk.type === "content_block_delta") {
209
- const delta = chunk.delta?.text ?? ""
210
- return delta.length === 0
211
- ? Stream.empty
212
- : Stream.succeed(
213
- LlmChunk.make({
214
- delta,
264
+ // Usage arrives in two events: `message_start` carries the input
265
+ // (and cache) counts, `message_delta` the output so far. The final
266
+ // chunk carries them together, so the seat is measured, not estimated.
267
+ const usage = Stream.unwrap(
268
+ Effect.map(Ref.make<AnthropicUsage | undefined>(undefined), (started) =>
269
+ httpClient
270
+ .postJsonStreamSSE(
271
+ `${baseUrl}/messages`,
272
+ JSON.stringify(request),
273
+ authHeaders(apiKey),
274
+ config.timeout
275
+ )
276
+ .pipe(
277
+ Stream.mapEffect(decodeStreamChunk),
278
+ Stream.mapEffect((chunk) =>
279
+ chunk.type === "message_start" && chunk.message?.usage !== undefined
280
+ ? Effect.as(Ref.set(started, chunk.message.usage), chunk)
281
+ : Effect.succeed(chunk)
282
+ ),
283
+ Stream.flatMap((chunk) => {
284
+ if (chunk.type === "content_block_delta") {
285
+ const delta = chunk.delta?.text ?? ""
286
+ return delta.length === 0
287
+ ? Stream.empty
288
+ : Stream.succeed(
289
+ LlmChunk.make({
290
+ delta,
291
+ metadata: {
292
+ provider: "anthropic",
293
+ model: config.model
294
+ }
295
+ })
296
+ )
297
+ }
298
+ const finishReason =
299
+ chunk.type === "message_delta"
300
+ ? (chunk.delta?.stop_reason ?? undefined)
301
+ : undefined
302
+ if (finishReason === undefined) {
303
+ return Stream.empty
304
+ }
305
+ return Stream.fromEffect(
306
+ Effect.map(Ref.get(started), (input) => {
307
+ const combined =
308
+ input === undefined && chunk.usage === undefined
309
+ ? undefined
310
+ : anthropicTokenUsage(
311
+ AnthropicUsage.make({
312
+ ...(input ?? {}),
313
+ ...(chunk.usage?.output_tokens === undefined
314
+ ? {}
315
+ : { output_tokens: chunk.usage.output_tokens })
316
+ })
317
+ )
318
+ return LlmChunk.make({
319
+ delta: "",
320
+ finishReason,
321
+ ...(combined === undefined ? {} : { usage: combined }),
215
322
  metadata: {
216
323
  provider: "anthropic",
217
324
  model: config.model
218
325
  }
219
326
  })
220
- )
221
- }
222
- const finishReason =
223
- chunk.type === "message_delta" ? (chunk.delta?.stop_reason ?? undefined) : undefined
224
- return finishReason === undefined
225
- ? Stream.empty
226
- : Stream.succeed(
227
- LlmChunk.make({
228
- delta: "",
229
- finishReason,
230
- metadata: {
231
- provider: "anthropic",
232
- model: config.model
233
- }
234
327
  })
235
328
  )
236
- })
329
+ })
330
+ )
237
331
  )
332
+ )
333
+ return usage
238
334
  })
239
335
  )
240
336
 
@@ -315,6 +411,7 @@ export const makeAnthropicProvider = (
315
411
  },
316
412
  executeWithTools,
317
413
  executeStructuredWithUsage,
318
- isAvailable
414
+ isAvailable,
415
+ capabilities: ConnectorCapabilities.make({ readOnlyEnforcement: "enforced", effort: "mapped" })
319
416
  })
320
417
  }
@@ -30,7 +30,8 @@ import {
30
30
  toolResultChunk,
31
31
  toolResultText,
32
32
  usageEventChunk,
33
- type JsonValue
33
+ type JsonValue,
34
+ effortWord
34
35
  } from "./CliSupport.ts"
35
36
 
36
37
  /**
@@ -51,7 +52,16 @@ export const claudeCliExtraArgs = (config: CliConnectorConfig): ReadonlyArray<st
51
52
  ...config.flags
52
53
  }
53
54
  : config.flags
54
- return [...optionalModelArgs(config.model), ...sortedFlagArgs(effectiveFlags)]
55
+ return [
56
+ ...optionalModelArgs(config.model),
57
+ // `--effort` is Claude Code's own vocabulary: low, medium, high, max.
58
+ ...(config.effort === undefined ? [] : ["--effort", effortWord(config.effort)]),
59
+ // `--bare` (Claude Code 2.1.81+) skips hooks, skills, plugins, MCP,
60
+ // auto memory and CLAUDE.md discovery: the documented headless mode for
61
+ // a repository the run does not trust (ADR 0029).
62
+ ...(config.isolated ? ["--bare"] : []),
63
+ ...sortedFlagArgs(effectiveFlags)
64
+ ]
55
65
  }
56
66
 
57
67
  export const claudeCliInitModel = (line: string): string | undefined => {
@@ -256,7 +266,9 @@ export const makeClaudeCliConnector = (
256
266
  askUser: true,
257
267
  approval: true,
258
268
  resumableSessions: true,
259
- readOnlyEnforcement: "enforced"
269
+ readOnlyEnforcement: "enforced",
270
+ effort: "mapped",
271
+ isolatedHeadless: "enforced"
260
272
  }),
261
273
  buildArgv: (prompt, _context) => ["claude", "--print", ...extraArgs, prompt],
262
274
  buildInteractiveArgv: (_context) => ["claude", ...extraArgs],
@@ -5,7 +5,7 @@ import * as Ref from "effect/Ref"
5
5
  import * as Schema from "effect/Schema"
6
6
  import * as Stream from "effect/Stream"
7
7
  import { ProviderError, type LlmError } from "../Errors.ts"
8
- import { LlmChunk, TokenUsage } from "../Models.ts"
8
+ import { LlmChunk, TokenUsage, type Effort } from "../Models.ts"
9
9
  import { classifyUsageLimit } from "../UsageLimits.ts"
10
10
 
11
11
  export type JsonValue = typeof Schema.Json.Type
@@ -68,6 +68,13 @@ export const sortedFlagArgs = (flags: Readonly<Record<string, string>>): Readonl
68
68
  export const optionalModelArgs = (model: string | undefined): ReadonlyArray<string> =>
69
69
  model === undefined ? [] : ["--model", model]
70
70
 
71
+ /**
72
+ * The harness's word for an llm4ts effort (ADR 0029): the same four words,
73
+ * except `max` on a harness whose top level is `xhigh`.
74
+ */
75
+ export const effortWord = (effort: Effort, top: "max" | "xhigh" = "max"): string =>
76
+ effort === "max" ? top : effort
77
+
71
78
  export const toolEventChunk = (name: string, input: JsonValue | undefined, id?: string): LlmChunk =>
72
79
  LlmChunk.make({
73
80
  delta: "",
@@ -17,6 +17,7 @@ import { collect } from "../Streaming.ts"
17
17
  import type { TemporaryFilesShape } from "../TemporaryFiles.ts"
18
18
  import { parseFromText, withSchemaHint } from "../StructuredOutput.ts"
19
19
  import {
20
+ effortWord,
20
21
  failClassifiedCliError,
21
22
  isJsonRecord,
22
23
  jsonField,
@@ -39,7 +40,19 @@ export const codexExtraArgs = (config: CliConnectorConfig): ReadonlyArray<string
39
40
  sandbox: "read-only"
40
41
  }
41
42
  : config.flags
42
- return [...optionalModelArgs(config.model), ...sortedFlagArgs(effectiveFlags)]
43
+ return [
44
+ ...optionalModelArgs(config.model),
45
+ // Codex reads its effort from config: `-c model_reasoning_effort="…"`,
46
+ // top level `xhigh`.
47
+ ...(config.effort === undefined
48
+ ? []
49
+ : ["-c", `model_reasoning_effort="${effortWord(config.effort, "xhigh")}"`]),
50
+ // Isolation is partial (ADR 0029): the project's `.codex/config.toml`
51
+ // is skipped unless the user trusted that path, and this turns off the
52
+ // project's AGENTS.md; MCP servers from the user's own config still load.
53
+ ...(config.isolated ? ["-c", "project_doc_max_bytes=0"] : []),
54
+ ...sortedFlagArgs(effectiveFlags)
55
+ ]
43
56
  }
44
57
 
45
58
  export const codexSchemaEnforceable = (schema: JsonSchema): boolean => {
@@ -224,7 +237,9 @@ export const makeCodexConnector = (
224
237
  // `sandbox: read-only` is an OS-level sandbox — a real capability gate.
225
238
  capabilities: ConnectorCapabilities.make({
226
239
  interactiveSessions: true,
227
- readOnlyEnforcement: "enforced"
240
+ readOnlyEnforcement: "enforced",
241
+ effort: "mapped",
242
+ isolatedHeadless: "partial"
228
243
  }),
229
244
  buildArgv: (prompt, _context) => ["codex", "exec", ...extraArgs, prompt],
230
245
  buildInteractiveArgv: (_context) => ["codex", ...extraArgs],
@@ -122,6 +122,8 @@ export class GeminiCliExecutionContext extends Schema.Class<GeminiCliExecutionCo
122
122
  sandbox: Schema.optionalKey(GeminiSandbox),
123
123
  turnLimit: Schema.optionalKey(Schema.Int),
124
124
  readOnly: Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(false))),
125
+ /** No project extensions (`-e none`); settings and GEMINI.md are still read (ADR 0029). */
126
+ isolated: Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(false))),
125
127
  /** The seat's own environment (a roster `env`), under llm4ts's own keys. */
126
128
  envVars: Schema.Record(Schema.String, Schema.String).pipe(
127
129
  Schema.withConstructorDefault(Effect.succeed<Readonly<Record<string, string>>>({}))
@@ -135,6 +137,7 @@ export const geminiCliExecutionContextFrom = (
135
137
  ...(config.workingDir === undefined ? {} : { cwd: config.workingDir }),
136
138
  ...(config.turnLimit === undefined ? {} : { turnLimit: config.turnLimit }),
137
139
  readOnly: config.readOnly,
140
+ isolated: config.isolated,
138
141
  envVars: config.envVars,
139
142
  ...(config.sandbox === undefined
140
143
  ? {}
@@ -226,7 +229,11 @@ export const buildGeminiArgs = (
226
229
  outputFormat,
227
230
  ...directories,
228
231
  ...(adminPolicyPath === undefined ? [] : ["--admin-policy", adminPolicyPath]),
229
- ...(context.sandbox === undefined ? [] : ["-s"])
232
+ ...(context.sandbox === undefined ? [] : ["-s"]),
233
+ // Partial isolation: no extensions; `.gemini/settings.json` and
234
+ // GEMINI.md have no off switch, and the ADR 0027 harness policy above
235
+ // already keeps the model out of llm4ts's own packages.
236
+ ...(context.isolated ? ["-e", "none"] : [])
230
237
  ]
231
238
  }
232
239
 
@@ -964,7 +971,8 @@ export const makeGeminiCliProvider = (
964
971
  // every seat that runs.
965
972
  capabilities: ConnectorCapabilities.make({
966
973
  interactiveSessions: true,
967
- readOnlyEnforcement: "enforced"
974
+ readOnlyEnforcement: "enforced",
975
+ isolatedHeadless: "partial"
968
976
  })
969
977
  })
970
978
 
@@ -44,6 +44,8 @@ export class OpenAIChatCompletionRequest extends Schema.Class<OpenAIChatCompleti
44
44
  stream: Schema.optionalKey(Schema.Boolean),
45
45
  response_format: Schema.optionalKey(OpenAIResponseFormat),
46
46
  chat_template_kwargs: Schema.optionalKey(OpenAIChatTemplateKwargs),
47
+ /** Reasoning effort (ADR 0029): low, medium, high, xhigh; sent only when asked. */
48
+ reasoning_effort: Schema.optionalKey(Schema.String),
47
49
  logprobs: Schema.optionalKey(Schema.Boolean),
48
50
  top_logprobs: Schema.optionalKey(Schema.Int)
49
51
  }) {}
@@ -7,6 +7,7 @@ import { AuthenticationError, ConfigError, ParseError, type LlmError } from "../
7
7
  import type { HttpClientShape } from "../HttpClient.ts"
8
8
  import type { StructuredResult } from "../LlmService.ts"
9
9
  import {
10
+ ConnectorCapabilities,
10
11
  ConnectorIds,
11
12
  LlmChunk,
12
13
  ToolCall,
@@ -18,6 +19,7 @@ import {
18
19
  type ToolDefinition
19
20
  } from "../Models.ts"
20
21
  import { parseFromText } from "../StructuredOutput.ts"
22
+ import { effortWord } from "./CliSupport.ts"
21
23
  import {
22
24
  OpenAIChatChunk,
23
25
  OpenAIChatCompletionRequest,
@@ -74,7 +76,8 @@ export const buildOpenAIChatRequest = (
74
76
  temperature: config.temperature ?? 0.7,
75
77
  stream,
76
78
  ...(config.maxTokens === undefined ? {} : { max_tokens: config.maxTokens }),
77
- ...(responseFormat === undefined ? {} : { response_format: responseFormat })
79
+ ...(responseFormat === undefined ? {} : { response_format: responseFormat }),
80
+ ...(config.effort === undefined ? {} : { reasoning_effort: effortWord(config.effort, "xhigh") })
78
81
  })
79
82
 
80
83
  export const buildOpenAIToolRequest = (
@@ -326,6 +329,7 @@ export const makeOpenAIProvider = (
326
329
  executeStreamWithHistory: (messages) => executeStreamRequest(openAIHistoryMessages(messages)),
327
330
  executeWithTools,
328
331
  executeStructuredWithUsage,
329
- isAvailable
332
+ isAvailable,
333
+ capabilities: ConnectorCapabilities.make({ readOnlyEnforcement: "enforced", effort: "mapped" })
330
334
  })
331
335
  }
@@ -6,6 +6,7 @@ import { ProviderError } 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
+ effortWord,
9
10
  cumulativeUsage,
10
11
  failClassifiedCliError,
11
12
  jsonBooleanField,
@@ -29,6 +30,11 @@ export const piExtraArgs = (config: CliConnectorConfig): ReadonlyArray<string> =
29
30
  // session, unsafe for a one-shot complete() call with no tool-loop
30
31
  // continuation (see the field's doc comment on CliConnectorConfig).
31
32
  ...(config.noTools ? ["--no-tools"] : config.readOnly ? ["--tools", "read"] : []),
33
+ // `--thinking` is pi's effort flag (off, minimal, low, medium, high, xhigh).
34
+ ...(config.effort === undefined ? [] : ["--thinking", effortWord(config.effort, "xhigh")]),
35
+ // Isolation is partial (ADR 0029): project extensions and skills stay
36
+ // out; pi still reads AGENTS.md from the working directory and its parents.
37
+ ...(config.isolated ? ["--no-extensions", "--no-skills"] : []),
32
38
  ...sortedFlagArgs(config.flags)
33
39
  ]
34
40
 
@@ -182,7 +188,9 @@ export const makePiConnector = (
182
188
  // capability removal, the same mechanism class as claude's `--tools`.
183
189
  capabilities: ConnectorCapabilities.make({
184
190
  interactiveSessions: true,
185
- readOnlyEnforcement: "enforced"
191
+ readOnlyEnforcement: "enforced",
192
+ effort: "mapped",
193
+ isolatedHeadless: "partial"
186
194
  }),
187
195
  buildArgv: (prompt, _context) => ["pi", "-p", ...extraArgs, prompt],
188
196
  buildInteractiveArgv: (_context) => ["pi", ...extraArgs],