@tangle-network/agent-app 0.44.18 → 0.44.21

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 (44) hide show
  1. package/.claude/skills/eval-campaign/SKILL.md +58 -95
  2. package/.claude/skills/surface-evolution/SKILL.md +29 -23
  3. package/README.md +8 -7
  4. package/dist/assistant/index.d.ts +2 -2
  5. package/dist/assistant/index.js +3 -3
  6. package/dist/{attachment-validation-Zw8eslhN.d.ts → attachment-validation-Dvc_Livy.d.ts} +1 -1
  7. package/dist/chat-routes/index.d.ts +17 -10
  8. package/dist/chat-routes/index.js +89 -12
  9. package/dist/chat-routes/index.js.map +1 -1
  10. package/dist/chat-store/index.d.ts +2 -2
  11. package/dist/chat-store/index.js +1 -1
  12. package/dist/{chunk-JXIQSGOV.js → chunk-2ZSSOYXP.js} +2 -6
  13. package/dist/chunk-2ZSSOYXP.js.map +1 -0
  14. package/dist/{chunk-VM6VLJH6.js → chunk-BI6NKSO4.js} +2 -2
  15. package/dist/{chunk-DV2FA2PW.js → chunk-C3SRFZGL.js} +18 -9
  16. package/dist/chunk-C3SRFZGL.js.map +1 -0
  17. package/dist/{chunk-34M7AUWO.js → chunk-M3UFMQ7D.js} +1 -1
  18. package/dist/chunk-M3UFMQ7D.js.map +1 -0
  19. package/dist/{chunk-7SMPAAIT.js → chunk-PRKSYTMQ.js} +3 -3
  20. package/dist/eval-campaign/index.d.ts +6 -8
  21. package/dist/eval-campaign/index.js +9 -5
  22. package/dist/eval-campaign/index.js.map +1 -1
  23. package/dist/forms/index.d.ts +333 -0
  24. package/dist/forms/index.js +343 -0
  25. package/dist/forms/index.js.map +1 -0
  26. package/dist/{parts-S1OINlRP.d.ts → parts-fyPPdDdK.d.ts} +3 -4
  27. package/dist/profile/index.d.ts +5 -6
  28. package/dist/profile/index.js +5 -3
  29. package/dist/profile/index.js.map +1 -1
  30. package/dist/sandbox/index.d.ts +3 -7
  31. package/dist/sandbox/index.js +1 -1
  32. package/dist/skills/index.d.ts +4 -3
  33. package/dist/skills/index.js +1 -1
  34. package/dist/skills-placement/index.d.ts +0 -1
  35. package/dist/skills-placement/index.js +1 -1
  36. package/dist/teams-react/index.js +3 -3
  37. package/dist/web-react/index.d.ts +3 -3
  38. package/dist/web-react/index.js +3 -3
  39. package/package.json +29 -18
  40. package/dist/chunk-34M7AUWO.js.map +0 -1
  41. package/dist/chunk-DV2FA2PW.js.map +0 -1
  42. package/dist/chunk-JXIQSGOV.js.map +0 -1
  43. /package/dist/{chunk-VM6VLJH6.js.map → chunk-BI6NKSO4.js.map} +0 -0
  44. /package/dist/{chunk-7SMPAAIT.js.map → chunk-PRKSYTMQ.js.map} +0 -0
@@ -1,116 +1,79 @@
1
1
  ---
2
2
  name: eval-campaign
3
- description: Wire a product agent's self-improvement loop (measure optimize gate ship) onto the shared @tangle-network/agent-app/eval-campaign scaffold. Use when adding or refactoring any product agent's eval/ loop.
3
+ description: Wire product measurement and improvement to Agent Eval campaigns, official optimizers, and explicit release decisions.
4
4
  ---
5
5
 
6
- # Wiring a product onto the eval-campaign scaffold
6
+ # Wire Eval Campaigns
7
7
 
8
- You are integrating a product agent's self-improvement loop. The loop **engine already exists** in the substrate — do not rebuild it. Your job is to supply the three things only the product knows, and call one function.
8
+ Use `@tangle-network/agent-app/eval-campaign` as the product-facing import.
9
+ It composes Agent Eval `0.133.0`.
10
+ Do not recreate execution, scoring, data splitting, or optimization.
9
11
 
10
- ## Mental model (read first)
12
+ ## Choose The Entry Point
11
13
 
12
- `selfImprove` (from `@tangle-network/agent-eval/contract`, re-exported here) owns the entire cycle:
14
+ | Job | Use |
15
+ |---|---|
16
+ | Measure one fixed agent | `runCampaign` |
17
+ | Optimize with official GEPA | `selfImprove` with `gepaOptimizationMethod(...)` |
18
+ | Optimize with Microsoft SkillOpt | `selfImprove` with `skillOptOptimizationMethod(...)` |
19
+ | Adapt another text optimizer | `selfImprove` with `externalTextOptimizationMethod(...)` |
20
+ | Let product code generate candidates | `selfImprove` with an explicit `SurfaceProposer` |
21
+ | Compare complete methods on the same cases | `compareOptimizationMethods` |
13
22
 
14
- - the **train/holdout split** from a flat `scenarios` array,
15
- - the **proposer** (default `gepaProposer` from your `mutationPrimitives`),
16
- - the **held-out production gate** (default `defaultProductionGate`, `deltaThreshold` 0.05),
17
- - **durable provenance** + optional hosted ingest,
18
- - every budget/seed/storage default.
23
+ There is no default candidate generator.
24
+ When `budget.generations` is greater than zero, pass exactly one of `method` or `proposer`.
19
25
 
20
- A product brings exactly three things:
26
+ ## Supply Product Inputs
21
27
 
22
- 1. **`scenarios`** your corpus (personas / cases / tasks) in the substrate `Scenario` shape.
23
- 2. **`agent`** `(surface, scenario, ctx) => artifact`: run your agent under the current surface (a system-prompt addendum the loop optimizes) and return the artifact your judge scores. Report real cost via `ctx.cost.observe(...)` so the backend-integrity guard sees a real run.
24
- 3. **`judge`** score an artifact on your rubric. Use `buildEnsembleJudge` (below) for a multi-model ensemble, or hand-write a `JudgeConfig` for a bespoke composite.
25
-
26
- Everything else is a default you override only when you have a reason.
27
-
28
- ## The one import
28
+ 1. Define `scenarios` with stable IDs.
29
+ 2. Implement `agent(surface, scenario, ctx)` against the real execution path.
30
+ 3. Report paid calls through `ctx.cost`.
31
+ 4. Define a `JudgeConfig`, or use `buildEnsembleJudge` for repeated independent judges.
32
+ 5. Keep method-selection cases separate from final comparison cases.
33
+ 6. Set a dollar limit and bounded concurrency.
34
+ 7. Promote only when `gateDecision` is `ship`.
29
35
 
30
36
  ```ts
31
37
  import {
32
- selfImprove,
33
38
  buildEnsembleJudge,
34
- type SelfImproveOptions,
35
- type JudgeVerdict,
39
+ selfImprove,
40
+ type OptimizationMethod,
36
41
  } from '@tangle-network/agent-app/eval-campaign'
37
- ```
38
-
39
- > Requires `@tangle-network/agent-eval >= 0.100.0` (peer; current published is 0.107.0). The scaffold composes the substrate downward; never import a product package from agent-eval (layering rule).
40
-
41
- ## Minimal wiring (copy, then fill the three blanks)
42
-
43
- ```ts
44
- const RUBRIC = ['accuracy', 'grounding', 'tone'] as const
45
- type Dim = (typeof RUBRIC)[number]
46
42
 
47
- const judge = buildEnsembleJudge<MyArtifact, MyScenario, Dim>({
48
- name: 'my-product',
49
- rubric: RUBRIC,
50
- judgeReps: 3, // 3 uncorrelated judges → inter-rater bands
51
- async scoreOne({ artifact, scenario, rep }) {
52
- const model = JUDGE_MODELS[rep % JUDGE_MODELS.length] // vary the model per rep
53
- try {
54
- const v = await callMyJudge(model, artifact, scenario) // → { accuracy, grounding, tone }
55
- return { model, perDimension: v, rationale: v.note, costUsd: v.cost }
56
- } catch (err) {
57
- return { model, perDimension: null, rationale: String(err) } // failure ≠ zero
58
- }
59
- },
60
- })
61
-
62
- const result = await selfImprove<MyScenario, MyArtifact>({
63
- scenarios: loadMyScenarios(), // YOU own
64
- agent: dispatchUnderSurface, // YOU own — (surface, scenario, ctx) => artifact
65
- judge, // built above
66
- baselineSurface: '', // the addendum the loop optimizes (start empty)
67
- mutationPrimitives: MY_DIRECTIVES, // the optimization levers (default proposer mutates toward these)
68
- runDir: process.env.MY_RUN_DIR, // a real path → durable provenance; omit → in-memory
69
- // budget / model / gate / hostedTenant all default — override only when needed
70
- })
71
-
72
- if (result.gate.decision === 'ship') await ship(result.winnerSurface)
43
+ async function optimize(
44
+ method: OptimizationMethod<MyScenario, MyArtifact>,
45
+ ) {
46
+ const judge = buildEnsembleJudge<MyArtifact, MyScenario, 'accuracy'>({
47
+ name: 'answer-quality',
48
+ rubric: ['accuracy'],
49
+ scoreOne: scoreOneAnswer,
50
+ })
51
+
52
+ return selfImprove({
53
+ scenarios,
54
+ selectionScenarios,
55
+ agent: runUnderSurface,
56
+ judge,
57
+ baselineSurface,
58
+ method,
59
+ budget: {
60
+ generations: 1,
61
+ dollars: 10,
62
+ maxConcurrency: 2,
63
+ },
64
+ })
65
+ }
73
66
  ```
74
67
 
75
- ## `buildEnsembleJudge` contract
76
-
77
- - `scoreOne` is called `judgeReps` times per artifact; **vary the model by `rep`** so the ensemble is uncorrelated (judges sharing a base model share its bias).
78
- - Return `{ model, perDimension: null }` to record a judge failure **without** killing the ensemble — the reducer means over survivors.
79
- - The reducer (`aggregateJudgeVerdicts`) **throws only if every rep failed** → the campaign records a failed cell, never a silent zero.
80
- - `weights` (partial) selects-and-weights named dimensions; default is uniform.
81
-
82
- ## Config reference (all `SelfImproveOptions`, all optional unless noted)
83
-
84
- | Field | Default | When to set |
85
- |---|---|---|
86
- | `scenarios` | — (required) | your corpus |
87
- | `agent` | — (required) | your dispatch under a surface |
88
- | `judge` | — (required) | `buildEnsembleJudge` or a `JudgeConfig` |
89
- | `baselineSurface` | — (required) | the surface the loop optimizes; start `''` |
90
- | `mutationPrimitives` | gepaProposer's own | your optimization levers (additive directives) |
91
- | `proposer` | `gepaProposer` | pass `evolutionaryProposer({ mutator })` for blind addendum rotation |
92
- | `gate` | `defaultProductionGate` (Δ 0.05) | `paretoSignificanceGate` for multi-objective; tune `deltaThreshold` for your rubric scale. To prove a held-out lift is real CONTENT and not just added prompt/mount FOOTPRINT, compose `neutralizationGate` (agent-eval >= 0.107.0, from `@tangle-network/agent-eval/campaign`) — a footprint-matched placebo arm. It needs `ctx.neutralizedJudgeScores` from `runImprovementLoop({ neutralize })`; `selfImprove`/this scaffold do not surface that option yet, so wire it at the `runImprovementLoop` level or re-export it in the scaffold first. |
93
- | `budget` | 3 gens × pop 2, 0.25 holdout | `budget.reps` (replicates → tighter CIs), `budget.promoteTopK`, `budget.holdoutScenarios` (explicit split), `budget.dollars` (cost cap) |
94
- | `expectUsage` | **`'assert'`** | the fail-loud backend-integrity guard. Leave at `'assert'` for real runs (a stub cell throws); set `'off'` ONLY for a deterministic offline/replay run |
95
- | `labeledStore` | off | capture every artifact + judge score (the dataset you ship + few-shot corpus); set `captureSource` (default `'eval-run'`) |
96
- | `analyzeGeneration` | — | the per-generation findings producer (EYES→HANDS) — plug a trace-analyst / HALO to refresh `ctx.findings` each round |
97
- | `runDir` | `mem://…` (non-durable) | a real path to persist provenance + spans |
98
- | `hostedTenant` | off | ship eval-run events to a hosted orchestrator |
99
- | `collectWorkerRecords` | — | return the per-call `RunRecord`s your agent accumulated → real backend-integrity verdict |
100
- | `onProgress` | — | stream baseline/generation/gate events to a UI |
101
-
102
- ## Fail-loud contract (do not break)
103
-
104
- - In `agent`, report real cost via `ctx.cost.observe(costUsd, label)` + `ctx.cost.observeTokens(...)`. A dispatch that reports `{0,0}` trips `expectUsage` — that is the honest "ran against a stub" signal; never paper over it.
105
- - A judge failure is `perDimension: null`, never a fabricated zero.
106
- - Train and holdout must both be non-empty (`selfImprove` derives the split; supply enough scenarios).
107
-
108
- ## Anti-patterns (these are what this scaffold deletes)
68
+ ## Reject Invalid Runs
109
69
 
110
- - Hand-rolling `runImprovementLoop({...})` + `emitLoopProvenance({...})` + a train/holdout split. That is ~100 lines of identical boilerplate per product. Call `selfImprove`.
111
- - A per-product copy of the judge-ensemble reducer (survivor-mean / disagreement / cost-sum). Use `buildEnsembleJudge` → `aggregateJudgeVerdicts`.
112
- - `import type` from a product package inside the scaffold or substrate (upward dependency — forbidden).
70
+ - Reject missing or zero usage on real model calls.
71
+ - Record a failed judge as `perDimension: null`; never invent a zero.
72
+ - Require non-empty training and final comparison sets.
73
+ - Never expose final comparison cases to an optimization method.
74
+ - Inspect paired results and complete cost accounting before making a claim.
113
75
 
114
- ## Where it lives in the product
76
+ ## Then consider
115
77
 
116
- One file: `eval/self-improve.ts`. It exports `runMyEval` (measure: `selfImprove` with `budget.generations = 0`, or `runCampaign`) and `runMySelfImprovement` (optimize: the wiring above). The product's harness/CLI calls these; nothing else duplicates the loop.
78
+ - Use `measurement-validation` when the metric has not separated known-good and known-bad cases.
79
+ - Use `surface-evolution` when the measurement is trusted and one production surface is ready to optimize.
@@ -1,35 +1,41 @@
1
1
  ---
2
2
  name: surface-evolution
3
- description: Optimize ONE evolvable surface (a prompt section / tool config) against a validated measurement, gate winners on held-out evidence plus a critical-dimension floor, and promote without offline/online drift. Use to run an Improve loop once eval-architect + measurement-validation pass.
3
+ description: Optimize one production agent surface with Agent Eval methods, separate data, bounded spend, and measured promotion.
4
4
  ---
5
5
 
6
- # Surface Evolution — run the gated loop, promote without drift
6
+ # Evolve One Surface
7
7
 
8
- You are a closed-loop controller for agent quality. **Sensor** = the eval (built by `eval-architect`, certified by `measurement-validation`). **Controller** = the proposer that proposes surface rewrites. **Actuator** = promotion (writing the surface to the live agent). **Safety interlock** = the gate. The interlock is the entire point: it prefers *under-promotion* to Goodhart. A loop that ships every apparent gain is worthless; a loop that ships only evidence-backed gains is the product.
8
+ Optimize one surface that production loads exactly as measured.
9
+ Keep security, approval, authorization, and compliance rules outside the editable surface.
9
10
 
10
- The engine exists in the substrate (`@tangle-network/agent-eval/contract` `selfImprove` / `runImprovementLoop`, `gepaProposer`, `defaultProductionGate`, `neutralizationGate`, `composeGate`) — re-exported via `@tangle-network/agent-app/eval-campaign`. **Do not rebuild it.** This skill is how you wire and run it safely.
11
+ ## Run
11
12
 
12
- ## Invariant (non-negotiable)
13
+ 1. Identify the exact source file or data object that production loads.
14
+ 2. Establish training, method-selection, and final comparison cases with no overlap.
15
+ 3. Choose one search path:
16
+ - Use `gepaOptimizationMethod(...)` for official GEPA.
17
+ - Use `skillOptOptimizationMethod(...)` for official SkillOpt.
18
+ - Use `externalTextOptimizationMethod(...)` for another maintained optimizer.
19
+ - Use `SurfaceProposer` only when product or Runtime code owns candidate generation.
20
+ 4. Pass the chosen `method` or `proposer` to `selfImprove`.
21
+ 5. Set explicit dollars, concurrency, and repetitions.
22
+ 6. Guard critical dimensions in the release policy.
23
+ 7. Pass `neutralize` when added prompt size could create a decorative lift.
24
+ 8. Promote only the exact returned winner when `gateDecision` is `ship`.
13
25
 
14
- 1. **Optimize exactly ONE surface that production renders identically.** The artifact you mutate offline must be the artifact the live agent loads — one source, rendered both places (e.g. an evolvable prompt section materialized from a single file into the live system prompt). If offline and online diverge, the lift is fictional the moment it ships.
15
- 2. **Gate promotion on a held-out split AND a critical-dimension floor.** Never promote a net composite gain that regresses a guarded dimension (safety, hallucination, the regulated invariant). A +10 composite that loses 30 on hallucination is a regression, not a win. **AND gate on a footprint placebo.** A held-out lift proves the candidate beat baseline; it does NOT prove the lift came from the surface's CONTENT rather than the extra prompt/mount FOOTPRINT it added. Compose `neutralizationGate` (from `@tangle-network/agent-eval/campaign`, wired via `runImprovementLoop({ neutralize })`) AFTER the significance gate — `composeGate(heldOutGate({...}), neutralizationGate({...}))` — so a candidate whose content-blanked, footprint-matched twin reproduces more than `maxDecorativeFraction` (default 0.5) of the lift is HELD as decorative, no matter how large or significant its raw lift.
16
- 3. **Budget is a hard ceiling and cost-aware** — skip cells beyond the ceiling, never abort. The user's spend maps to generations × candidates × reps: $0.20 buys one quick generation; $50 buys a wide search with tight CIs.
17
- 4. **Never evolve a frozen surface.** The regulated invariants — human-in-the-loop, the compliance gate, auth/RBAC — are off-limits. Declare exactly what is evolvable; everything else the loop must not touch.
26
+ ## Required Checks
18
27
 
19
- > Worked result (this is the skill working): a GEPA run on the legal addendum proposed a "multi-jurisdictional divergence handling" section, diagnosed from the worst *training* personas. The gate guarded `hallucination_free` (no regression) and required held-out significance. When the held-out evidence came back incomplete, it **refused to ship** — exactly right. Trust > lift.
28
+ - Prove the metric rejects a known-bad candidate before spending on search.
29
+ - Prove each compared cell executed and reported usage.
30
+ - Require paired final-case evidence.
31
+ - Reject a winner that regresses any protected dimension.
32
+ - After promotion, compare the live bytes with the selected surface.
33
+ - Repeat the final comparison on fresh cases before claiming a durable gain.
20
34
 
21
- ## Judgment (figure this out per product)
35
+ Stop when added candidates no longer improve selection results within the budget.
36
+ If failures require new tools, data, or execution behavior, change the architecture instead of continuing prompt search.
22
37
 
23
- - Which surface is safe to evolve (a guidance section, a tool description, a config knob) vs frozen (the invariants above)?
24
- - How to scope budget to the gap — one generation to confirm a hunch, many to search a wide space?
25
- - When has it converged or plateaued? If surface-tuning is exhausted and the gap is architectural, escalate — don't keep spending on a surface that's maxed.
38
+ ## Then consider
26
39
 
27
- ## Self-test
28
-
29
- - **After a promotion, the live agent renders the exact winning surface** — diff the deployed artifact against the promoted one. They must be byte-identical.
30
- - **The held-out lift reproduces on a fresh run** — a one-shot gain is noise until it repeats.
31
- - **The gate's rejections are honest** — a held verdict carries a stated reason ("0 valid paired runs", "regressed guarded dimension"), never a silent pass and never a lift over partial data.
32
-
33
- ## Evolves-by
34
-
35
- Production outcomes of shipped surfaces (did the held-out lift actually hold live?) feed back into the driver's mutation priors and the gate's thresholds. A surface that lifted offline but flatlined live tells you the held-out set wasn't representative — widen it. See `skill-evolution`.
40
+ - Use `measurement-validation` when the metric or final-case split is not yet trustworthy.
41
+ - Use `skill-evolution` when repeated runs expose a durable improvement to this operating procedure.
package/README.md CHANGED
@@ -35,15 +35,16 @@ pnpm add @tangle-network/agent-eval @tangle-network/agent-integrations
35
35
 
36
36
  | Peer | Required by | Range |
37
37
  |---|---|---|
38
- | `@tangle-network/agent-eval` | `/eval`, `/eval-campaign`, `/profile`, `/knowledge` | `>=0.100.0` |
39
- | `@tangle-network/agent-runtime` | `/runtime`, `/chat-routes` | `>=0.79.3` |
38
+ | `@tangle-network/agent-eval` | `/eval`, `/eval-campaign`, `/profile`, `/knowledge` | `0.133.2` |
39
+ | `@tangle-network/agent-runtime` | `/runtime`, `/chat-routes` | `0.107.4` |
40
40
  | `@tangle-network/agent-integrations` | `/integrations` | `>=0.44.0` |
41
- | `@tangle-network/agent-interface` | `/interactions`, `/chat-store`, `/harness` | `>=0.15.0` |
42
- | `@tangle-network/sandbox` | `/sandbox`, `/profile`, `/skills` | `>=0.9.7` |
43
- | `@tangle-network/agent-knowledge` | `/knowledge-loop` | `>=1.7.0` |
44
- | `@tangle-network/agent-profile-materialize` | `/skills-placement` | `>=0.6.0` |
41
+ | `@tangle-network/agent-interface` | `/interactions`, `/chat-store`, `/harness` | `0.36.0` |
42
+ | `@tangle-network/sandbox` | `/sandbox` | `0.15.1` |
43
+ | `@tangle-network/agent-knowledge` | `/knowledge-loop` | `6.1.4` |
44
+ | `@tangle-network/agent-profile-materialize` | `/skills-placement` | `0.9.0` |
45
+ | `@tangle-network/sandbox-ui` | `/brand`, `/studio-react`, `/work-product-react` | `0.90.1` |
45
46
 
46
- All of these except `agent-eval`, `agent-integrations`, and `agent-interface` are declared **optional** peers, so a product that never imports the subpath installs nothing. `driveSandboxTurn` (`/sandbox`) calls `box.driveTurn`, which the SDK added in **0.10.5** — above the declared floor, so pin `@tangle-network/sandbox >= 0.10.5` yourself if you use it.
47
+ All of these except `agent-eval`, `agent-integrations`, and `agent-interface` are declared **optional** peers, so a product that never imports the subpath installs nothing.
47
48
 
48
49
  Modules that import no engine package (`/tools`, `/web`, `/crypto`, `/redact`, `/stream`, `/billing`, `/tangle` — the last two take their client as a structural contract) need no peers.
49
50
 
@@ -4,7 +4,7 @@ import { ToolDetailRenderers, ChatUiMessage } from '../web-react/index.js';
4
4
  import '../contract-CEewO6DI.js';
5
5
  import '@tangle-network/agent-interface';
6
6
  import '../plans/index.js';
7
- import '../parts-S1OINlRP.js';
7
+ import '../parts-fyPPdDdK.js';
8
8
  import '../types-DB82fktc.js';
9
9
  import '../agent-activity-C8ZG0F0M.js';
10
10
  import '../flow-types-CJxEmaRy.js';
@@ -12,7 +12,7 @@ import '../queue-vRI0Qx3X.js';
12
12
  import '../sandbox-terminal-ChNEdHF8.js';
13
13
  import '../catalog/index.js';
14
14
  import '../harness/index.js';
15
- import '../attachment-validation-Zw8eslhN.js';
15
+ import '../attachment-validation-Dvc_Livy.js';
16
16
  import '../stream-normalizer-DnuqkZvw.js';
17
17
 
18
18
  /**
@@ -3,13 +3,13 @@ import {
3
3
  ChatMessages,
4
4
  ModelPicker,
5
5
  ProviderLogo
6
- } from "../chunk-7SMPAAIT.js";
6
+ } from "../chunk-PRKSYTMQ.js";
7
7
  import "../chunk-FBVLEGEG.js";
8
8
  import "../chunk-YTMKRL3L.js";
9
9
  import "../chunk-GEYACSFW.js";
10
10
  import "../chunk-HCOROIRT.js";
11
- import "../chunk-VM6VLJH6.js";
12
- import "../chunk-DV2FA2PW.js";
11
+ import "../chunk-BI6NKSO4.js";
12
+ import "../chunk-C3SRFZGL.js";
13
13
  import "../chunk-ZVEEWGDK.js";
14
14
  import "../chunk-5EPIPT4V.js";
15
15
  import "../chunk-3ZK5IJSW.js";
@@ -1,4 +1,4 @@
1
- import { M as FileMention } from './parts-S1OINlRP.js';
1
+ import { M as FileMention } from './parts-fyPPdDdK.js';
2
2
 
3
3
  /**
4
4
  * `createSandboxFileIndexRoute` — server side of `@`-file-mentions
@@ -1,6 +1,6 @@
1
- import { C as ChatMessagePart, N as ChatTurnRequestPayload, O as ChatTurnPartInput, P as ChatTurnFilePartInput, b as ChatAttachmentPart, a as ChatAttachmentKind, g as ChatMentionPart } from '../parts-S1OINlRP.js';
2
- export { Q as ChatAttachmentInput, f as ChatMentionKind, R as ChatTurnInputError, T as ChatTurnTextPartInput, U as DISPATCH_MAX_MEDIA_PARTS, V as DISPATCH_MAX_PARTS, W as DISPATCH_REQUEST_MAX_BYTES, X as DISPATCH_STRUCTURAL_RESERVE_BYTES, M as FileMention, Y as FileMentionsToPartsOptions, Z as INLINE_PARTS_MAX_BYTES, _ as MENTION_MAX_COUNT, $ as ProducerErrorEvent, a0 as ProducerNoticeEvent, a1 as ProducerPassthroughEvent, a2 as ProducerPassthroughEventType, a3 as ProducerReasoningEvent, a4 as ProducerTextEvent, a5 as ProducerToolCallEvent, a6 as ProducerToolResultEvent, a7 as ProducerUsageEvent, a8 as ProducerWireEvent, a9 as SandboxMentionPathCheck, aa as assertPromptPartsWithinCap, ab as base64WireLen, ac as buildMentionPromptBlock, ad as chatTurnRequestInit, ae as fileMentionsToParts, af as formatBytes, ag as mediaTypeForMentionPath, ah as mentionKindForPath, ai as parseChatTurnParts, aj as parseFileMentions, ak as promptPartsByteSize, al as validateSandboxMentionPath } from '../parts-S1OINlRP.js';
3
- export { A as ALLOWED_ATTACHMENT_SNIFFED_MIMES, a as ATTACHMENT_ACCEPT, b as ATTACHMENT_MAX_COUNT, c as AttachmentTypeCheckResult, C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, d as FileIndexCache, e as FileIndexReadyResponse, f as FileIndexResponse, g as FileIndexWarmingResponse, M as MAX_ATTACHMENT_TOTAL_BYTES, h as MAX_BINARY_ATTACHMENT_BYTES, i as MAX_TEXT_ATTACHMENT_BYTES, S as SandboxFileTreeSource, j as SandboxTreeFile, k as SandboxTreeResult, l as SniffResult, m as attachmentSizeErrorMessage, n as attachmentTotalSizeErrorMessage, o as checkAttachmentType, p as createSandboxFileIndexRoute, s as sanitizeAttachmentFileName, q as sniffBinary } from '../attachment-validation-Zw8eslhN.js';
1
+ import { C as ChatMessagePart, N as ChatTurnRequestPayload, O as ChatTurnPartInput, P as ChatTurnFilePartInput, b as ChatAttachmentPart, a as ChatAttachmentKind, g as ChatMentionPart } from '../parts-fyPPdDdK.js';
2
+ export { Q as ChatAttachmentInput, f as ChatMentionKind, R as ChatTurnInputError, T as ChatTurnTextPartInput, U as DISPATCH_MAX_MEDIA_PARTS, V as DISPATCH_MAX_PARTS, W as DISPATCH_REQUEST_MAX_BYTES, X as DISPATCH_STRUCTURAL_RESERVE_BYTES, M as FileMention, Y as FileMentionsToPartsOptions, Z as INLINE_PARTS_MAX_BYTES, _ as MENTION_MAX_COUNT, $ as ProducerErrorEvent, a0 as ProducerNoticeEvent, a1 as ProducerPassthroughEvent, a2 as ProducerPassthroughEventType, a3 as ProducerReasoningEvent, a4 as ProducerTextEvent, a5 as ProducerToolCallEvent, a6 as ProducerToolResultEvent, a7 as ProducerUsageEvent, a8 as ProducerWireEvent, a9 as SandboxMentionPathCheck, aa as assertPromptPartsWithinCap, ab as base64WireLen, ac as buildMentionPromptBlock, ad as chatTurnRequestInit, ae as fileMentionsToParts, af as formatBytes, ag as mediaTypeForMentionPath, ah as mentionKindForPath, ai as parseChatTurnParts, aj as parseFileMentions, ak as promptPartsByteSize, al as validateSandboxMentionPath } from '../parts-fyPPdDdK.js';
3
+ export { A as ALLOWED_ATTACHMENT_SNIFFED_MIMES, a as ATTACHMENT_ACCEPT, b as ATTACHMENT_MAX_COUNT, c as AttachmentTypeCheckResult, C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, d as FileIndexCache, e as FileIndexReadyResponse, f as FileIndexResponse, g as FileIndexWarmingResponse, M as MAX_ATTACHMENT_TOTAL_BYTES, h as MAX_BINARY_ATTACHMENT_BYTES, i as MAX_TEXT_ATTACHMENT_BYTES, S as SandboxFileTreeSource, j as SandboxTreeFile, k as SandboxTreeResult, l as SniffResult, m as attachmentSizeErrorMessage, n as attachmentTotalSizeErrorMessage, o as checkAttachmentType, p as createSandboxFileIndexRoute, s as sanitizeAttachmentFileName, q as sniffBinary } from '../attachment-validation-Dvc_Livy.js';
4
4
  import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime';
5
5
  import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
6
6
  import { PersistedChatMessageForTurn } from '../stream/index.js';
@@ -1262,7 +1262,8 @@ interface CreateUploadRouteOptions {
1262
1262
  inlineMaxBytes?: number;
1263
1263
  /** Hard per-file cap. Default {@link UPLOAD_MAX_FILE_BYTES}. */
1264
1264
  maxFileBytes?: number;
1265
- /** Workspace directory for path-ref files. Default `'uploads'`. */
1265
+ /** Absolute workspace directory for path-ref files.
1266
+ * Default `'/workspace/uploads'`. */
1266
1267
  uploadDir?: string;
1267
1268
  }
1268
1269
  /** One uploaded file, ready for the composer chip and the turn body. */
@@ -1526,9 +1527,9 @@ declare function createAttachmentUploadRoute(options: CreateAttachmentUploadRout
1526
1527
  * blocks); each attachment or mention becomes one media part. An attachment
1527
1528
  * (read from the product store via the injected reader) draws the inline byte
1528
1529
  * budget first; a mention (read from the LIVE box) takes what is left. A file
1529
- * inlines as a `data:` URI when it fits the remaining budget, otherwise demotes
1530
- * to an in-box path part so the whole request stays under the proxy cap. Every
1531
- * media part is deduped by its resolved absolute path. This module only
1530
+ * inlines as a `data:` URI when it fits the remaining budget, otherwise uses
1531
+ * the in-box file reference supported by the current sandbox prompt contract.
1532
+ * Every media part is deduped by its resolved absolute path. This module only
1532
1533
  * produces the parts array; the caller decides when a turn dispatches parts
1533
1534
  * instead of a plain string.
1534
1535
  *
@@ -1565,6 +1566,12 @@ type SandboxMentionReadOutcome = {
1565
1566
  type ReadSandboxMentionFn = (box: SandboxExecChannel, absolutePath: string, options: {
1566
1567
  readBytes: boolean;
1567
1568
  }) => Promise<SandboxMentionReadOutcome>;
1569
+ /**
1570
+ * Convert the browser-safe chat attachment contract into the current sandbox
1571
+ * prompt contract. Images carry exactly one URL or path; generic files carry a
1572
+ * filename and URL.
1573
+ */
1574
+ declare function normalizeChatPromptForSandbox(prompt: string | readonly ChatTurnPartInput[]): string | PromptInputPart[];
1568
1575
  /** Build input parameters for dispatching chat message parts including text, attachments, mentions, and history */
1569
1576
  interface BuildDispatchPartsInput {
1570
1577
  text: string;
@@ -1583,7 +1590,7 @@ interface BuildDispatchPartsInput {
1583
1590
  /** The product's workspace/tenant key, passed to `readAttachment`. */
1584
1591
  scopeId: string;
1585
1592
  /** Maps an attachment's store-relative path to the in-box absolute path a
1586
- * path-based part references (same seam style as `fileMentionsToParts`'s
1593
+ * local media reference uses (same style as `fileMentionsToParts`'s
1587
1594
  * `resolvePath`). */
1588
1595
  resolveAttachmentPath: (path: string) => string;
1589
1596
  /** Maps a mention's workspace-relative path to its in-box absolute path.
@@ -1593,7 +1600,7 @@ interface BuildDispatchPartsInput {
1593
1600
  /** The turn's already-ensured box — required when `mentions` is non-empty
1594
1601
  * (mention bytes are read from the live box, not the store). */
1595
1602
  box?: SandboxExecChannel;
1596
- /** Force every media part to a path reference, skipping all inlining. */
1603
+ /** Force every media part to a local file reference, skipping all inlining. */
1597
1604
  forcePath?: boolean;
1598
1605
  /** REQUIRED store reader for attachment content — no default (the product
1599
1606
  * owns its store; see {@link ReadAttachmentFn}). */
@@ -1693,4 +1700,4 @@ interface PromoteAgentFilePartOptions {
1693
1700
  /** Promote a part of an agent file with optional byte limits and MIME type detection */
1694
1701
  declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
1695
1702
 
1696
- export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type EmptyTurnRetryInfo, type FilePartPromotionOutcome, MAX_EMPTY_TURN_RETRIES, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, promoteAgentFilePart, resolveChatAttachments, resolveEmptyTurnRetries, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, summarizeFailoverReason, withDurableChatProjection };
1703
+ export { type AssistantDraftSnapshot, type AssistantDraftStore, type AssistantDraftWriter, type AssistantDraftWriterOptions, type AssistantRowValues, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentUploadAuthorization, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatRouteEvent, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, type ChatTurnCompleteInput, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, type ChatTurnModelFailover, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateAttachmentUploadRouteOptions, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DetachedTurnFinal, type DetachedTurnOptions, type DetachedTurnParts, type DetachedTurnResult, type DispatchPartsOutcome, type DraftPersistenceTuning, type DraftStoredMessage, type EmptyTurnRetryInfo, type FilePartPromotionOutcome, MAX_EMPTY_TURN_RETRIES, type ModelFailoverStreamHandle, type ModelFailoverStreamOptions, type ModelFallbackInfo, type OpenModelStream, PROMOTE_MAX_FILE_BYTES, type PromoteAgentFilePartOptions, type PromoteFilePartResult, PromptInputPart, type RawAgentFilePart, type ReadAttachmentFn, type ReadSandboxMentionFn, type ResolveChatAttachmentsOptions, type ResolveChatAttachmentsResult, type SandboxChatProducerOptions, type SandboxUploadSink, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, type WriteAttachmentFn, assistantRowIdForTurn, buildDispatchParts, bytesToBase64, classifyTerminalFailure, createAssistantDraftWriter, createAttachmentUploadRoute, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, isCommittingSandboxEvent, isDraftContentEvent, normalizeChatPromptForSandbox, promoteAgentFilePart, resolveChatAttachments, resolveEmptyTurnRetries, rowIdOf, runDetachedTurn, sanitizeUploadFilename, sniffMimeFromName, storeSupportsDraftPersistence, streamWithModelFailover, summarizeFailoverReason, withDurableChatProjection };
@@ -11,7 +11,7 @@ import {
11
11
  createSandboxFileIndexRoute,
12
12
  sanitizeAttachmentFileName,
13
13
  sniffBinary
14
- } from "../chunk-VM6VLJH6.js";
14
+ } from "../chunk-BI6NKSO4.js";
15
15
  import {
16
16
  coalesceDeltas,
17
17
  createBufferedTurnTap,
@@ -55,7 +55,7 @@ import {
55
55
  promptPartsByteSize,
56
56
  toChatMessageParts,
57
57
  validateSandboxMentionPath
58
- } from "../chunk-DV2FA2PW.js";
58
+ } from "../chunk-C3SRFZGL.js";
59
59
  import "../chunk-ZVEEWGDK.js";
60
60
  import {
61
61
  asRecord,
@@ -93,7 +93,7 @@ import {
93
93
  flattenHistory,
94
94
  readSandboxBinaryBytes,
95
95
  statSandboxFileSize
96
- } from "../chunk-JXIQSGOV.js";
96
+ } from "../chunk-2ZSSOYXP.js";
97
97
  import "../chunk-LWSJK546.js";
98
98
  import "../chunk-CQZSAR77.js";
99
99
  import "../chunk-ICOHEZK6.js";
@@ -310,8 +310,9 @@ function validateTurnBody(body, maxInlinePartBytes) {
310
310
  const content = rawContent.trim();
311
311
  const fileParts = parseChatTurnParts(body.parts);
312
312
  const mentions = parseFileMentions(body.mentions);
313
- if (!content && fileParts.length === 0 && mentions.length === 0) {
314
- throw new ChatTurnInputError("Missing content (send text, parts, mentions, or any combination)");
313
+ const hasAttachments = Array.isArray(body.attachments) && body.attachments.length > 0;
314
+ if (!content && fileParts.length === 0 && mentions.length === 0 && !hasAttachments) {
315
+ throw new ChatTurnInputError("Missing content (send text, parts, mentions, attachments, or any combination)");
315
316
  }
316
317
  assertPromptPartsWithinCap(fileParts, maxInlinePartBytes);
317
318
  let turnId;
@@ -1693,6 +1694,14 @@ function withDurableChatProjection(producer, projection, log = (message, meta) =
1693
1694
  // src/chat-routes/upload.ts
1694
1695
  var UPLOAD_INLINE_MAX_BYTES = 700 * 1024;
1695
1696
  var UPLOAD_MAX_FILE_BYTES = 8 * 1024 * 1024;
1697
+ var DEFAULT_UPLOAD_DIRECTORY = "/workspace/uploads";
1698
+ function normalizeUploadDirectory(value) {
1699
+ const withoutTrailingSlash = value.trim().replace(/\/+$/, "");
1700
+ if (!withoutTrailingSlash.startsWith("/") || withoutTrailingSlash.length === 0) return null;
1701
+ const segments = withoutTrailingSlash.slice(1).split("/");
1702
+ if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) return null;
1703
+ return `/${segments.join("/")}`;
1704
+ }
1696
1705
  function sanitizeUploadFilename(name) {
1697
1706
  const base = name.split(/[\\/]/).pop() ?? "file";
1698
1707
  const safe = base.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^\.+/, "_");
@@ -1716,7 +1725,7 @@ function createUploadRoute(options) {
1716
1725
  const auth = await options.authorize({ request });
1717
1726
  if (!auth.ok) return auth.response;
1718
1727
  const sink = auth.sink ?? null;
1719
- const uploadDir = (auth.uploadDir ?? options.uploadDir ?? "uploads").replace(/\/+$/, "");
1728
+ const configuredUploadDir = auth.uploadDir ?? options.uploadDir ?? DEFAULT_UPLOAD_DIRECTORY;
1720
1729
  let form;
1721
1730
  try {
1722
1731
  form = await request.formData();
@@ -1767,6 +1776,14 @@ function createUploadRoute(options) {
1767
1776
  `${name} is ${file.size}B, over the ${inlineMaxBytes}B inline cap, and no sandbox is available to hold it`
1768
1777
  );
1769
1778
  }
1779
+ const uploadDir = normalizeUploadDirectory(configuredUploadDir);
1780
+ if (!uploadDir) {
1781
+ return uploadError(
1782
+ 500,
1783
+ "INVALID_UPLOAD_DIRECTORY",
1784
+ 'uploadDir must be an absolute path without empty, "." or ".." segments'
1785
+ );
1786
+ }
1770
1787
  const path = `${uploadDir}/${id}-${name}`;
1771
1788
  const base64 = bytesToBase64(new Uint8Array(await file.arrayBuffer()));
1772
1789
  await sink.write(path, base64, { encoding: "base64" });
@@ -2179,6 +2196,7 @@ function createAttachmentUploadRoute(options) {
2179
2196
  }
2180
2197
 
2181
2198
  // src/chat-routes/dispatch-parts.ts
2199
+ import { pathToFileURL } from "url";
2182
2200
  function byteLen(value) {
2183
2201
  return new TextEncoder().encode(value).length;
2184
2202
  }
@@ -2196,10 +2214,58 @@ async function readSandboxMention(box, absolutePath, options) {
2196
2214
  }
2197
2215
  function violatesUrlPathXor(part) {
2198
2216
  if (part.type === "text") return false;
2199
- const hasUrl = typeof part.url === "string" && part.url.startsWith("data:");
2200
- const hasPath = typeof part.path === "string" && part.path.startsWith("/");
2217
+ const hasUrl = typeof part.url === "string" && (part.url.startsWith("data:") || part.url.startsWith("file://"));
2218
+ const hasPath = "path" in part && typeof part.path === "string" && part.path.startsWith("/");
2219
+ if (part.type === "file") return !hasUrl || hasPath;
2201
2220
  return hasUrl === hasPath;
2202
2221
  }
2222
+ function sandboxFileUrl(absolutePath) {
2223
+ if (!absolutePath.startsWith("/")) return void 0;
2224
+ try {
2225
+ return pathToFileURL(absolutePath).href;
2226
+ } catch {
2227
+ return void 0;
2228
+ }
2229
+ }
2230
+ function normalizeChatPromptForSandbox(prompt) {
2231
+ if (typeof prompt === "string") return prompt;
2232
+ return prompt.map((part) => {
2233
+ if (part.type === "text") return part;
2234
+ if ("content" in part) {
2235
+ throw new ChatTurnInputError("Sandbox prompt parts do not accept inline content; provide a URL or path");
2236
+ }
2237
+ const url = typeof part.url === "string" && part.url.length > 0 ? part.url : void 0;
2238
+ const path = typeof part.path === "string" && part.path.length > 0 ? part.path : void 0;
2239
+ if (Boolean(url) === Boolean(path)) {
2240
+ throw new ChatTurnInputError(`Sandbox ${part.type} parts require exactly one URL or path`);
2241
+ }
2242
+ if (path && !path.startsWith("/")) {
2243
+ throw new ChatTurnInputError(`Sandbox ${part.type} paths must be absolute: ${path}`);
2244
+ }
2245
+ if (part.type === "image") {
2246
+ return {
2247
+ type: "image",
2248
+ ...part.filename ? { filename: part.filename } : {},
2249
+ ...part.mediaType ? { mediaType: part.mediaType } : {},
2250
+ ...url ? { url } : { path }
2251
+ };
2252
+ }
2253
+ const filename = part.filename?.trim();
2254
+ if (!filename) {
2255
+ throw new ChatTurnInputError("Sandbox file parts require a filename");
2256
+ }
2257
+ const fileUrl = url ?? sandboxFileUrl(path);
2258
+ if (!fileUrl) {
2259
+ throw new ChatTurnInputError(`Sandbox file paths must be absolute: ${path}`);
2260
+ }
2261
+ return {
2262
+ type: "file",
2263
+ filename,
2264
+ ...part.mediaType ? { mediaType: part.mediaType } : {},
2265
+ url: fileUrl
2266
+ };
2267
+ });
2268
+ }
2203
2269
  function readResultToBase64(read) {
2204
2270
  if (typeof read.base64 === "string") return read.base64;
2205
2271
  if (read.bytes) return bytesToBase64(read.bytes);
@@ -2270,7 +2336,11 @@ async function buildDispatchParts(input) {
2270
2336
  parts.push(inlinePart);
2271
2337
  runningInline += cost;
2272
2338
  } else {
2273
- parts.push({ type: "file", path: absPath });
2339
+ const url = sandboxFileUrl(absPath);
2340
+ if (!url) {
2341
+ return { succeeded: false, error: `resolved attachment path must be absolute: ${absPath}` };
2342
+ }
2343
+ parts.push({ type: "file", filename: attachment.name, mediaType: fileMediaType, url });
2274
2344
  }
2275
2345
  }
2276
2346
  if (mentions.length > 0 && !input.box) {
@@ -2315,9 +2385,15 @@ async function buildDispatchParts(input) {
2315
2385
  continue;
2316
2386
  }
2317
2387
  }
2318
- parts.push(
2319
- isImage && mediaType ? { type: "image", filename: mention.name, mediaType, path: absPath } : { type: "file", path: absPath }
2320
- );
2388
+ if (isImage && mediaType) {
2389
+ parts.push({ type: "image", filename: mention.name, mediaType, path: absPath });
2390
+ } else {
2391
+ const url = sandboxFileUrl(absPath);
2392
+ if (!url) {
2393
+ return { succeeded: false, error: `resolved mention path must be absolute: ${absPath}` };
2394
+ }
2395
+ parts.push({ type: "file", filename: mention.name, url });
2396
+ }
2321
2397
  }
2322
2398
  for (const part of parts) {
2323
2399
  if (violatesUrlPathXor(part)) {
@@ -2379,6 +2455,7 @@ export {
2379
2455
  isDraftContentEvent,
2380
2456
  mediaTypeForMentionPath,
2381
2457
  mentionKindForPath,
2458
+ normalizeChatPromptForSandbox,
2382
2459
  parseChatTurnParts,
2383
2460
  parseFileMentions,
2384
2461
  promoteAgentFilePart,