@agent-native/core 0.94.2 → 0.95.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 (85) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/pr-visual-recap.mdx +20 -12
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/artifact-response.ts +154 -1
  6. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +3 -0
  7. package/corpus/core/src/agent/engine/registry.ts +6 -1
  8. package/corpus/core/src/agent/engine/types.ts +2 -0
  9. package/corpus/core/src/cli/code-agent-executor.ts +61 -1
  10. package/corpus/core/src/cli/pr-visual-recap-workflow.ts +1 -1
  11. package/corpus/core/src/cli/recap.ts +262 -25
  12. package/corpus/core/src/client/settings/UsageSection.tsx +26 -5
  13. package/corpus/core/src/index.ts +2 -0
  14. package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
  15. package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
  16. package/corpus/core/src/integrations/adapters/slack.ts +77 -7
  17. package/corpus/core/src/integrations/types.ts +30 -0
  18. package/corpus/core/src/integrations/webhook-handler.ts +54 -18
  19. package/corpus/core/src/scripts/call-agent.ts +1 -0
  20. package/corpus/core/src/server/request-context.ts +2 -0
  21. package/corpus/core/src/usage/store.ts +82 -20
  22. package/corpus/templates/plan/actions/record-recap-usage-cost.ts +49 -0
  23. package/corpus/templates/plan/actions/record-recap-usage.ts +15 -22
  24. package/dist/a2a/artifact-response.d.ts.map +1 -1
  25. package/dist/a2a/artifact-response.js +127 -1
  26. package/dist/a2a/artifact-response.js.map +1 -1
  27. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  28. package/dist/agent/engine/ai-sdk-engine.js +3 -0
  29. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  30. package/dist/agent/engine/registry.d.ts +1 -1
  31. package/dist/agent/engine/registry.d.ts.map +1 -1
  32. package/dist/agent/engine/registry.js +2 -0
  33. package/dist/agent/engine/registry.js.map +1 -1
  34. package/dist/agent/engine/types.d.ts +2 -0
  35. package/dist/agent/engine/types.d.ts.map +1 -1
  36. package/dist/agent/engine/types.js.map +1 -1
  37. package/dist/cli/code-agent-executor.d.ts +2 -0
  38. package/dist/cli/code-agent-executor.d.ts.map +1 -1
  39. package/dist/cli/code-agent-executor.js +52 -3
  40. package/dist/cli/code-agent-executor.js.map +1 -1
  41. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  42. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  43. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  44. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  45. package/dist/cli/recap.d.ts +22 -2
  46. package/dist/cli/recap.d.ts.map +1 -1
  47. package/dist/cli/recap.js +199 -22
  48. package/dist/cli/recap.js.map +1 -1
  49. package/dist/client/settings/UsageSection.d.ts.map +1 -1
  50. package/dist/client/settings/UsageSection.js +12 -3
  51. package/dist/client/settings/UsageSection.js.map +1 -1
  52. package/dist/collab/struct-routes.d.ts +1 -1
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js.map +1 -1
  56. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  57. package/dist/integrations/a2a-continuation-processor.js +76 -21
  58. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  59. package/dist/integrations/a2a-continuations-store.d.ts +4 -1
  60. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  61. package/dist/integrations/a2a-continuations-store.js +130 -6
  62. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  63. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  64. package/dist/integrations/adapters/slack.js +56 -7
  65. package/dist/integrations/adapters/slack.js.map +1 -1
  66. package/dist/integrations/types.d.ts +25 -0
  67. package/dist/integrations/types.d.ts.map +1 -1
  68. package/dist/integrations/types.js.map +1 -1
  69. package/dist/integrations/webhook-handler.js +54 -16
  70. package/dist/integrations/webhook-handler.js.map +1 -1
  71. package/dist/resources/handlers.d.ts +2 -2
  72. package/dist/scripts/call-agent.d.ts.map +1 -1
  73. package/dist/scripts/call-agent.js +1 -0
  74. package/dist/scripts/call-agent.js.map +1 -1
  75. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  76. package/dist/server/request-context.d.ts +2 -0
  77. package/dist/server/request-context.d.ts.map +1 -1
  78. package/dist/server/request-context.js.map +1 -1
  79. package/dist/server/transcribe-voice.d.ts +1 -1
  80. package/dist/usage/store.d.ts +21 -0
  81. package/dist/usage/store.d.ts.map +1 -1
  82. package/dist/usage/store.js +54 -19
  83. package/dist/usage/store.js.map +1 -1
  84. package/docs/content/pr-visual-recap.mdx +20 -12
  85. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2166
31
- - template files: 5364
31
+ - template files: 5365
@@ -1,5 +1,19 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.95.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2308575: Add an OpenAI-compatible PR visual recap backend for DeepSeek, Kimi, and other compatible providers.
8
+
9
+ ## 0.94.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 48d8471: Keep Slack agent task cards active through deferred cross-agent work, show durable progress, and deliver the true final result instead of treating interim artifacts as complete.
14
+ - 48d8471: Keep completed realtime voice turns in their originating chat, restore that chat safely when voice ends, and expose live audio activity to the voice dock.
15
+ - 48d8471: Accept canonical Content document links returned by successful read-only actions as A2A artifact proof while continuing to reject mismatched or unproven URLs.
16
+
3
17
  ## 0.94.2
4
18
 
5
19
  ### Patch Changes
@@ -7,7 +7,7 @@ description: "A GitHub Action that runs your repo's visual-recap skill on every
7
7
 
8
8
  PR Visual Recap is a GitHub Action that turns every pull request into a **visual code review**. On each push, an LLM coding agent runs the latest bundled [`visual-recap`](/docs/template-plan) skill (or your repo's committed copy when `VISUAL_RECAP_SKILL_SOURCE=repo`) against the PR diff, publishes a structured recap plan to the hosted Plans app, shows an informational `Visual Recap` check while it runs, and upserts **one sticky PR comment** that links to the interactive plan with an **inline screenshot** embedded right in the comment.
9
9
 
10
- This is not a deterministic diff renderer. The action invokes a real coding agent (Claude Code CLI by default, or OpenAI Codex CLI) that reads the change, decides what matters, and authors the recap by calling the Plans MCP tool `create-visual-recap` — the same tool the `/visual-recap` slash command uses. You get a high-altitude, schema/API/before-after view of the change instead of a wall of raw diff.
10
+ This is not a deterministic diff renderer. The action invokes a real coding agent (Claude Code CLI by default, OpenAI Codex CLI, or Agent-Native Code with an OpenAI-compatible provider) that reads the change, decides what matters, and authors the recap by calling the Plans MCP tool `create-visual-recap` — the same tool the `/visual-recap` slash command uses. You get a high-altitude, schema/API/before-after view of the change instead of a wall of raw diff.
11
11
 
12
12
  The recap is **informational and non-blocking**. It creates a check row so reviewers can see that generation is in progress, but it is not a required check, it never blocks the PR, and it never replaces reading the actual diff. The sticky comment is a review aid, not a sign-off.
13
13
 
@@ -36,7 +36,7 @@ On each PR push, the workflow:
36
36
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
37
37
  <div class="diagram-node">
38
38
  Coding agent<br /><small class="diagram-muted"
39
- >Claude Code / Codex reads diff</small
39
+ >configured backend reads diff</small
40
40
  >
41
41
  </div>
42
42
  <div class="diagram-arrow diagram-muted" aria-hidden="true">&rarr;</div>
@@ -127,28 +127,32 @@ pins its committed `visual-recap` folder, set the repository variable
127
127
 
128
128
  Choose which coding agent runs the skill with the `VISUAL_RECAP_AGENT` repository variable:
129
129
 
130
- | `VISUAL_RECAP_AGENT` | Coding agent | Required API key |
131
- | -------------------- | ---------------- | ------------------- |
132
- | `claude` _(default)_ | Claude Code CLI | `ANTHROPIC_API_KEY` |
133
- | `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` |
130
+ | `VISUAL_RECAP_AGENT` | Coding agent | Required API key | Required variables |
131
+ | -------------------- | --------------------------------------- | ---------------------- | --------------------------------------------- |
132
+ | `claude` _(default)_ | Claude Code CLI | `ANTHROPIC_API_KEY` | |
133
+ | `codex` | OpenAI Codex CLI | `OPENAI_API_KEY` | — |
134
+ | `openai-compatible` | Agent-Native Code + compatible provider | `VISUAL_RECAP_API_KEY` | `VISUAL_RECAP_BASE_URL`, `VISUAL_RECAP_MODEL` |
134
135
 
135
136
  If the variable is unset, the action uses `claude`.
136
137
 
137
138
  ## Model and reasoning
138
139
 
139
- Beyond the backend, two repository variables tune _how_ the agent runs:
140
+ Beyond the backend, these repository variables tune how the agent runs:
140
141
 
141
- - **`VISUAL_RECAP_MODEL`** pins the model passed to the CLI (`--model`) — for example `gpt-5.6-sol` for Codex, or a Claude model id. Leave it unset to use the CLI's own default model.
142
+ - **`VISUAL_RECAP_MODEL`** pins the model passed to the CLI (`--model`) — for example `gpt-5.6-sol` for Codex, a Claude model id, or `deepseek-chat` for DeepSeek. It is required for `openai-compatible`; Claude and Codex may leave it unset to use their CLI default.
142
143
  - **`VISUAL_RECAP_REASONING`** sets the reasoning depth: `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`. It applies to the Codex backend; Claude's reasoning is model-driven, so this variable is ignored there.
144
+ - **`VISUAL_RECAP_BASE_URL`** selects the HTTP(S) base URL for the `openai-compatible` backend. Use the provider's OpenAI-compatible endpoint, such as DeepSeek or Kimi, without embedding credentials in the URL.
143
145
  - **`VISUAL_RECAP_SKILL_SOURCE`** controls prompt freshness: `auto`/unset uses the latest bundled skill guidance, while `repo` pins to the committed repo-local `visual-recap` skill folder.
144
146
 
145
147
  For example, to run the recap on Codex with GPT-5.6 Sol at high reasoning, set the repository variables `VISUAL_RECAP_AGENT=codex`, `VISUAL_RECAP_MODEL=gpt-5.6-sol`, and `VISUAL_RECAP_REASONING=high`.
146
148
 
149
+ For DeepSeek, set `VISUAL_RECAP_AGENT=openai-compatible`, `VISUAL_RECAP_BASE_URL=https://api.deepseek.com/v1`, `VISUAL_RECAP_MODEL=deepseek-chat`, and the `VISUAL_RECAP_API_KEY` secret. Kimi works the same way with its OpenAI-compatible base URL and model id. Other OpenAI-compatible providers use the same three settings.
150
+
147
151
  ## Secrets and variables
148
152
 
149
153
  Set these in your repository's **Settings → Secrets and variables → Actions**.
150
154
 
151
- ### Secrets (only two required)
155
+ ### Secrets for the default backend
152
156
 
153
157
  | Secret | Purpose |
154
158
  | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
@@ -190,8 +194,10 @@ real token.
190
194
  | Secret / variable | Default | When you need it |
191
195
  | ------------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
192
196
  | `OPENAI_API_KEY` | — | Secret. Set together with `VISUAL_RECAP_AGENT=codex` to run the recap with Codex instead. |
193
- | `VISUAL_RECAP_AGENT` | `claude` | Variable. Selects the coding-agent backend (`claude` or `codex`). |
194
- | `VISUAL_RECAP_MODEL` | each CLI's default | Variable. Pins the model e.g. `gpt-5.6-sol` for Codex, or a Claude model id. Unset uses the CLI's own default. |
197
+ | `VISUAL_RECAP_API_KEY` | | Secret. Set with `VISUAL_RECAP_AGENT=openai-compatible` for DeepSeek, Kimi, or another OpenAI-compatible provider. |
198
+ | `VISUAL_RECAP_AGENT` | `claude` | Variable. Selects the coding-agent backend (`claude`, `codex`, or `openai-compatible`). |
199
+ | `VISUAL_RECAP_BASE_URL` | required for compatible backend | Variable. HTTP(S) base URL for the OpenAI-compatible backend; credentials are rejected. |
200
+ | `VISUAL_RECAP_MODEL` | required for compatible backend | Variable. Provider model id for OpenAI-compatible backends; optional for Claude/Codex, where unset uses the CLI default. |
195
201
  | `VISUAL_RECAP_REASONING` | each model's default | Variable. Reasoning depth: `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`. Applies to the Codex backend. |
196
202
  | `RECAP_CLI_VERSION` | `latest` | Variable. Pins the `@agent-native/core` CLI version the workflow installs — e.g. `1.5.0`. See [Version pinning](#version-pinning-copy-variant). |
197
203
  | `PLAN_RECAP_APP_URL` | `https://plan.agent-native.com` | Secret. Only when self-hosting the Plans app at a different origin. |
@@ -214,7 +220,7 @@ This also means you can merge the workflow file **before** the secrets exist: wi
214
220
 
215
221
  If you want to generate recaps for fork PRs, a second workflow file is available: `.github/workflows/pr-visual-recap-fork.yml`. It uses `pull_request_target` (which runs with base-repo secrets) but never checks out or executes fork code. Trusted fork authors with GitHub author association `OWNER`, `MEMBER`, or `COLLABORATOR` run automatically. Outside fork PRs require an explicit **per-head maintainer opt-in** via a fresh `recap` label event before the recap agent runs.
216
222
 
217
- To install it, copy the file from [BuilderIO/agent-native](https://github.com/BuilderIO/agent-native/blob/main/.github/workflows/pr-visual-recap-fork.yml) into your repo's `.github/workflows/` directory alongside the existing `pr-visual-recap.yml`. The same secrets (`PLAN_RECAP_TOKEN`, `ANTHROPIC_API_KEY`) apply.
223
+ To install it, copy the file from [BuilderIO/agent-native](https://github.com/BuilderIO/agent-native/blob/main/.github/workflows/pr-visual-recap-fork.yml) into your repo's `.github/workflows/` directory alongside the existing `pr-visual-recap.yml`. The same backend configuration applies, including `VISUAL_RECAP_API_KEY`, `VISUAL_RECAP_BASE_URL`, and `VISUAL_RECAP_MODEL` when using an OpenAI-compatible provider.
218
224
 
219
225
  <Diagram id="doc-block-h3lne0" title="Fork PR consent gate" summary={"Fork PRs get no secrets by default; trusted authors run automatically, and outside contributors require a fresh maintainer recap label."}>
220
226
 
@@ -454,10 +460,12 @@ jobs:
454
460
  PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}
455
461
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
456
462
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
463
+ VISUAL_RECAP_API_KEY: ${{ secrets.VISUAL_RECAP_API_KEY }}
457
464
  PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL }}
458
465
  with:
459
466
  agent: ${{ vars.VISUAL_RECAP_AGENT || 'claude' }}
460
467
  model: ${{ vars.VISUAL_RECAP_MODEL || '' }}
468
+ base-url: ${{ vars.VISUAL_RECAP_BASE_URL || '' }}
461
469
  reasoning: ${{ vars.VISUAL_RECAP_REASONING || '' }}
462
470
  skill-source: ${{ vars.VISUAL_RECAP_SKILL_SOURCE || 'auto' }}
463
471
  # cli-version: "latest" # pin to a specific @agent-native/core version
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.94.2",
3
+ "version": "0.95.0",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -228,6 +228,132 @@ function contentDatabaseSubmissionArtifact(
228
228
  };
229
229
  }
230
230
 
231
+ function documentUrlForId(
232
+ parsed: Record<string, unknown>,
233
+ id: string,
234
+ additionalCandidates: Array<string | undefined> = [],
235
+ options: { requireContentOrigin?: boolean } = {},
236
+ ): string | undefined {
237
+ const candidates = [
238
+ stringValue(parsed.url),
239
+ stringValue(parsed.urlPath),
240
+ stringValue(parsed.deepLink),
241
+ stringValue(parsed.pageUrl),
242
+ stringValue(parsed.documentUrl),
243
+ ...additionalCandidates,
244
+ ].filter((value): value is string => !!value);
245
+
246
+ return candidates.find((candidate) => {
247
+ if (!artifactUrlReferencesId(candidate, "document", id)) return false;
248
+ return !options.requireContentOrigin || isContentDocumentUrl(candidate);
249
+ });
250
+ }
251
+
252
+ function isContentDocumentUrl(rawUrl: string): boolean {
253
+ try {
254
+ return new URL(rawUrl).origin === "https://content.agent-native.com";
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+
260
+ function addDocumentReadArtifact(
261
+ documents: Map<string, CreatedDocumentArtifact>,
262
+ parsed: Record<string, unknown>,
263
+ options: {
264
+ allowWithoutUrl: boolean;
265
+ additionalUrlCandidates?: Array<string | undefined>;
266
+ requireContentOrigin?: boolean;
267
+ },
268
+ ): void {
269
+ const id = stringValue(parsed.documentId) ?? stringValue(parsed.id);
270
+ if (!id) return;
271
+
272
+ const url = documentUrlForId(parsed, id, options.additionalUrlCandidates, {
273
+ requireContentOrigin: options.requireContentOrigin,
274
+ });
275
+ if (!url && !options.allowWithoutUrl) return;
276
+
277
+ documents.set(id, {
278
+ id,
279
+ title: stringValue(parsed.title) ?? stringValue(parsed.name),
280
+ url,
281
+ });
282
+ }
283
+
284
+ function addContentDatabaseReadArtifacts(
285
+ documents: Map<string, CreatedDocumentArtifact>,
286
+ parsed: Record<string, unknown>,
287
+ ): void {
288
+ const resultUrls = [
289
+ stringValue(parsed.url),
290
+ stringValue(parsed.urlPath),
291
+ stringValue(parsed.deepLink),
292
+ ];
293
+ const database = asRecord(parsed.database);
294
+ if (database) {
295
+ addDocumentReadArtifact(documents, database, {
296
+ allowWithoutUrl: true,
297
+ requireContentOrigin: true,
298
+ additionalUrlCandidates: resultUrls,
299
+ });
300
+ } else {
301
+ // Unavailable database reads still return a documentId, but they do not
302
+ // prove that the page exists and must not authorize an artifact URL.
303
+ if (parsed.available !== false) {
304
+ addDocumentReadArtifact(documents, parsed, {
305
+ allowWithoutUrl: true,
306
+ requireContentOrigin: true,
307
+ });
308
+ }
309
+ }
310
+
311
+ if (!Array.isArray(parsed.items)) return;
312
+ for (const item of parsed.items) {
313
+ const itemRecord = asRecord(item);
314
+ const document = asRecord(itemRecord?.document);
315
+ if (!document) continue;
316
+ addDocumentReadArtifact(documents, document, {
317
+ allowWithoutUrl: true,
318
+ requireContentOrigin: true,
319
+ additionalUrlCandidates: [
320
+ stringValue(itemRecord?.url),
321
+ stringValue(itemRecord?.urlPath),
322
+ stringValue(itemRecord?.deepLink),
323
+ ],
324
+ });
325
+ }
326
+ }
327
+
328
+ function isGenericReadTool(tool: string): boolean {
329
+ return /^(?:find|get|list|query|read|search)-/i.test(tool);
330
+ }
331
+
332
+ function addGenericDocumentReadArtifact(
333
+ documents: Map<string, CreatedDocumentArtifact>,
334
+ parsed: Record<string, unknown>,
335
+ ): void {
336
+ // Unknown read actions are accepted only when their result pairs a document
337
+ // ID with a canonical page URL containing that exact ID. An ID by itself is
338
+ // insufficient, preserving the fabrication guard for unrelated actions.
339
+ addDocumentReadArtifact(documents, parsed, {
340
+ allowWithoutUrl: false,
341
+ requireContentOrigin: true,
342
+ });
343
+
344
+ const document = asRecord(parsed.document);
345
+ if (!document) return;
346
+ addDocumentReadArtifact(documents, document, {
347
+ allowWithoutUrl: false,
348
+ requireContentOrigin: true,
349
+ additionalUrlCandidates: [
350
+ stringValue(parsed.url),
351
+ stringValue(parsed.urlPath),
352
+ stringValue(parsed.deepLink),
353
+ ],
354
+ });
355
+ }
356
+
231
357
  function addImageArtifact(
232
358
  images: Map<string, CreatedImageArtifact>,
233
359
  parsed: Record<string, unknown>,
@@ -387,7 +513,6 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
387
513
 
388
514
  if (
389
515
  toolResult.tool === "create-document" ||
390
- toolResult.tool === "get-document" ||
391
516
  toolResult.tool === "update-document"
392
517
  ) {
393
518
  const id = stringValue(parsed.id);
@@ -401,6 +526,34 @@ function collectArtifacts(results: A2AToolResultSummary[]): {
401
526
  continue;
402
527
  }
403
528
 
529
+ if (
530
+ toolResult.tool === "get-document" ||
531
+ toolResult.tool === "get-content-document"
532
+ ) {
533
+ const document = asRecord(parsed.document);
534
+ addDocumentReadArtifact(documents, document ?? parsed, {
535
+ allowWithoutUrl: true,
536
+ requireContentOrigin: true,
537
+ additionalUrlCandidates: document
538
+ ? [
539
+ stringValue(parsed.url),
540
+ stringValue(parsed.urlPath),
541
+ stringValue(parsed.deepLink),
542
+ ]
543
+ : [],
544
+ });
545
+ continue;
546
+ }
547
+
548
+ if (toolResult.tool === "get-content-database") {
549
+ addContentDatabaseReadArtifacts(documents, parsed);
550
+ continue;
551
+ }
552
+
553
+ if (isGenericReadTool(toolResult.tool)) {
554
+ addGenericDocumentReadArtifact(documents, parsed);
555
+ }
556
+
404
557
  if (
405
558
  toolResult.tool === "create-deck" ||
406
559
  toolResult.tool === "duplicate-deck"
@@ -205,6 +205,7 @@ class AISDKEngine implements AgentEngine {
205
205
  readonly label: string;
206
206
  readonly defaultModel: string;
207
207
  readonly supportedModels: readonly string[];
208
+ readonly preserveCustomModels: boolean;
208
209
  readonly capabilities: EngineCapabilities;
209
210
 
210
211
  private readonly provider: AISDKProvider;
@@ -219,6 +220,8 @@ class AISDKEngine implements AgentEngine {
219
220
  this.label = `${capitalize(provider)} (AI SDK)`;
220
221
  this.defaultModel = config.model ?? PROVIDER_DEFAULT_MODELS[provider];
221
222
  this.supportedModels = PROVIDER_SUPPORTED_MODELS[provider];
223
+ this.preserveCustomModels =
224
+ provider === "openai" && Boolean(config.baseUrl);
222
225
  this.capabilities = PROVIDER_CAPABILITIES[provider];
223
226
  this.apiKey =
224
227
  config.apiKey ??
@@ -180,12 +180,17 @@ function findLatestSupportedVersionMatch(
180
180
  }
181
181
 
182
182
  export function normalizeModelForEngine(
183
- engine: Pick<AgentEngine, "name" | "defaultModel" | "supportedModels">,
183
+ engine: Pick<
184
+ AgentEngine,
185
+ "name" | "defaultModel" | "supportedModels" | "preserveCustomModels"
186
+ >,
184
187
  model: string | null | undefined,
185
188
  ): string {
186
189
  const candidate = typeof model === "string" ? model.trim() : "";
187
190
  if (!candidate) return engine.defaultModel;
188
191
 
192
+ if (engine.preserveCustomModels) return candidate;
193
+
189
194
  if (engine.supportedModels.length === 0) return candidate;
190
195
 
191
196
  if (candidate === "auto" || engine.supportedModels.includes(candidate)) {
@@ -297,6 +297,8 @@ export interface AgentEngine {
297
297
  readonly defaultModel: string;
298
298
  /** Models this engine supports */
299
299
  readonly supportedModels: readonly string[];
300
+ /** Whether the configured endpoint accepts provider-defined model ids. */
301
+ readonly preserveCustomModels?: boolean;
300
302
  /** Capability flags used to gate provider-specific features */
301
303
  readonly capabilities: EngineCapabilities;
302
304
 
@@ -101,6 +101,10 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
101
101
  const MAX_TOOL_OUTPUT_CHARS = 50_000;
102
102
  const MAX_FILE_READ_CHARS = 120_000;
103
103
  const CODEX_CLI_ENGINE_NAME = "codex-cli";
104
+ const RECAP_SOURCE_TOOL_PROFILE = "recap-source";
105
+ const RECAP_SOURCE_OUTPUT_FILE = "recap-source.json";
106
+
107
+ type CodeAgentToolProfile = typeof RECAP_SOURCE_TOOL_PROFILE;
104
108
 
105
109
  /**
106
110
  * Number of most-recent transcript events reconstructed as native
@@ -237,6 +241,9 @@ export async function executeCodeAgentRun(
237
241
  options.reasoningEffort ?? metadataReasoningEffort(existing);
238
242
  const cwd = existing.cwd || process.cwd();
239
243
  const permissionMode = existing.permissionMode ?? "full-auto";
244
+ const toolProfile = resolveCodeAgentToolProfile(
245
+ process.env.AGENT_NATIVE_CODE_TOOL_PROFILE,
246
+ );
240
247
 
241
248
  // Holds structured metadata emitted by the coding tools side-channel.
242
249
  // Keyed by tool name; consumed when the matching tool_start / tool_done fires.
@@ -254,8 +261,11 @@ export async function executeCodeAgentRun(
254
261
  // Stream incremental bash output to stdout for the terminal smoother
255
262
  options.stdout?.write(chunk);
256
263
  },
264
+ toolProfile,
257
265
  );
258
- const mcpManager = await startCodeAgentMcpManager(existing.id);
266
+ const mcpManager = toolProfile
267
+ ? null
268
+ : await startCodeAgentMcpManager(existing.id);
259
269
  if (mcpManager) {
260
270
  Object.assign(
261
271
  actions,
@@ -392,6 +402,7 @@ export async function executeCodeAgentRun(
392
402
  }),
393
403
  );
394
404
  loopUsage = usageResult ?? null;
405
+ writeCodeAgentUsageSnapshot(cwd, loopUsage);
395
406
  // Persist cumulative token totals from this turn into the run record so
396
407
  // the UI can display per-run usage statistics.
397
408
  if (loopUsage) {
@@ -1663,6 +1674,7 @@ function createLocalCodeAgentActions(
1663
1674
  meta: StructuredToolMetadata,
1664
1675
  ) => void,
1665
1676
  onBashOutputChunk?: (chunk: string) => void,
1677
+ toolProfile?: CodeAgentToolProfile,
1666
1678
  ): Record<string, ActionEntry> {
1667
1679
  const actions = createCodingToolRegistry({
1668
1680
  cwd,
@@ -1701,6 +1713,24 @@ function createLocalCodeAgentActions(
1701
1713
  return null;
1702
1714
  },
1703
1715
  });
1716
+ if (toolProfile === RECAP_SOURCE_TOOL_PROFILE) {
1717
+ const allowedOutput = path.resolve(cwd, RECAP_SOURCE_OUTPUT_FILE);
1718
+ const writeAction = actions.write;
1719
+ return {
1720
+ read: actions.read,
1721
+ write: {
1722
+ ...writeAction,
1723
+ run: async (args, context) => {
1724
+ const requestedPath =
1725
+ args && typeof args.path === "string" ? args.path : "";
1726
+ if (path.resolve(cwd, requestedPath) !== allowedOutput) {
1727
+ return `Error: only ${RECAP_SOURCE_OUTPUT_FILE} may be written in the recap-source tool profile.`;
1728
+ }
1729
+ return writeAction.run(args, context);
1730
+ },
1731
+ },
1732
+ };
1733
+ }
1704
1734
  if (permissionMode === "read-only") {
1705
1735
  return {
1706
1736
  bash: actions.bash,
@@ -1710,6 +1740,15 @@ function createLocalCodeAgentActions(
1710
1740
  return actions;
1711
1741
  }
1712
1742
 
1743
+ function resolveCodeAgentToolProfile(
1744
+ value: string | undefined,
1745
+ ): CodeAgentToolProfile | undefined {
1746
+ const normalized = value?.trim();
1747
+ if (!normalized) return undefined;
1748
+ if (normalized === RECAP_SOURCE_TOOL_PROFILE) return normalized;
1749
+ throw new Error(`Unsupported Agent-Native Code tool profile: ${normalized}`);
1750
+ }
1751
+
1713
1752
  export type CodeAgentCommandPermission =
1714
1753
  | { kind: "read" }
1715
1754
  | { kind: "write" }
@@ -1863,6 +1902,27 @@ function getPendingApproval(runId: string): PendingCodeAgentApproval | null {
1863
1902
  };
1864
1903
  }
1865
1904
 
1905
+ export function writeCodeAgentUsageSnapshot(
1906
+ cwd: string,
1907
+ usage: AgentLoopUsage | null,
1908
+ ): void {
1909
+ const configuredPath = process.env.AGENT_NATIVE_CODE_USAGE_FILE?.trim();
1910
+ if (!configuredPath || !usage) return;
1911
+
1912
+ const outputPath = path.isAbsolute(configuredPath)
1913
+ ? configuredPath
1914
+ : path.resolve(cwd, configuredPath);
1915
+ try {
1916
+ fs.writeFileSync(outputPath, `${JSON.stringify(usage)}\n`, {
1917
+ encoding: "utf8",
1918
+ mode: 0o600,
1919
+ });
1920
+ } catch {
1921
+ // Usage reporting is best-effort and must not turn a completed agent run
1922
+ // into a failed recap when the optional sidecar cannot be written.
1923
+ }
1924
+ }
1925
+
1866
1926
  // --------------- Token usage accumulator ---------------
1867
1927
 
1868
1928
  interface StoredTokenUsage {