@tangle-network/agent-app 0.43.46 → 0.43.48

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 (40) hide show
  1. package/dist/assistant/index.d.ts +3 -2
  2. package/dist/assistant/index.js +3 -2
  3. package/dist/assistant/index.js.map +1 -1
  4. package/dist/chat-routes/index.d.ts +407 -160
  5. package/dist/chat-routes/index.js +512 -102
  6. package/dist/chat-routes/index.js.map +1 -1
  7. package/dist/chat-store/index.d.ts +3 -2
  8. package/dist/chat-store/index.js +19 -1
  9. package/dist/chat-store/index.js.map +1 -1
  10. package/dist/chunk-23LUHHHN.js +282 -0
  11. package/dist/chunk-23LUHHHN.js.map +1 -0
  12. package/dist/{chunk-4AUQIAYU.js → chunk-5MG74GVQ.js} +2 -281
  13. package/dist/chunk-5MG74GVQ.js.map +1 -0
  14. package/dist/chunk-BQV42AFK.js +99 -0
  15. package/dist/chunk-BQV42AFK.js.map +1 -0
  16. package/dist/{chunk-6E2XJSCT.js → chunk-JGYOYY5D.js} +65 -2
  17. package/dist/chunk-JGYOYY5D.js.map +1 -0
  18. package/dist/{chunk-7VMUOD3G.js → chunk-PHIXZC6Y.js} +1 -1
  19. package/dist/chunk-PHIXZC6Y.js.map +1 -0
  20. package/dist/{file-index-Bn6sitKb.d.ts → file-index-b26ee-_R.d.ts} +1 -1
  21. package/dist/index.d.ts +4 -2
  22. package/dist/index.js +38 -22
  23. package/dist/{parts-1_3y2JmR.d.ts → parts-IB-Kbb7z.d.ts} +146 -2
  24. package/dist/sandbox/index.d.ts +1 -1
  25. package/dist/stale-turn-lock-C8Na1cFZ.d.ts +149 -0
  26. package/dist/stream/index.d.ts +5 -192
  27. package/dist/stream/index.js +14 -12
  28. package/dist/stream-normalizer-DWvtmY6F.d.ts +48 -0
  29. package/dist/teams/index.js +9 -9
  30. package/dist/teams/invitations-api.js +7 -7
  31. package/dist/turn-buffer-C9mEgoop.d.ts +146 -0
  32. package/dist/turn-stream/index.d.ts +551 -0
  33. package/dist/turn-stream/index.js +837 -0
  34. package/dist/turn-stream/index.js.map +1 -0
  35. package/dist/web-react/index.d.ts +4 -3
  36. package/dist/web-react/index.js +24 -2
  37. package/package.json +6 -1
  38. package/dist/chunk-4AUQIAYU.js.map +0 -1
  39. package/dist/chunk-6E2XJSCT.js.map +0 -1
  40. package/dist/chunk-7VMUOD3G.js.map +0 -1
@@ -1,12 +1,21 @@
1
- import { A as ChatTurnRequestPayload, B as ChatTurnPartInput, e as ChatMessagePart, D as ChatTurnFilePartInput } from '../parts-1_3y2JmR.js';
2
- export { c as ChatMentionKind, E as ChatTurnInputError, G as ChatTurnTextPartInput, F as FileMention, H as FileMentionsToPartsOptions, I as INLINE_PARTS_MAX_BYTES, M as MENTION_MAX_COUNT, J as SandboxMentionPathCheck, K as assertPromptPartsWithinCap, L as buildMentionPromptBlock, N as chatTurnRequestInit, O as fileMentionsToParts, P as mediaTypeForMentionPath, Q as mentionKindForPath, R as parseChatTurnParts, T as parseFileMentions, U as promptPartsByteSize, V as validateSandboxMentionPath } from '../parts-1_3y2JmR.js';
1
+ import { L as ChatTurnRequestPayload, M as ChatTurnPartInput, g as ChatMessagePart, N as ChatTurnFilePartInput, a as ChatAttachmentPart, f as ChatMentionPart, C as ChatAttachmentKind } from '../parts-IB-Kbb7z.js';
2
+ export { O as ChatAttachmentInput, e as ChatMentionKind, P as ChatTurnInputError, Q as ChatTurnTextPartInput, R as DISPATCH_MAX_MEDIA_PARTS, T as DISPATCH_MAX_PARTS, U as DISPATCH_REQUEST_MAX_BYTES, V as DISPATCH_STRUCTURAL_RESERVE_BYTES, K as FileMention, W as FileMentionsToPartsOptions, X as INLINE_PARTS_MAX_BYTES, Y as MENTION_MAX_COUNT, Z as SandboxMentionPathCheck, _ as assertPromptPartsWithinCap, $ as base64WireLen, a0 as buildMentionPromptBlock, a1 as chatTurnRequestInit, a2 as fileMentionsToParts, a3 as formatBytes, a4 as mediaTypeForMentionPath, a5 as mentionKindForPath, a6 as parseChatTurnParts, a7 as parseFileMentions, a8 as promptPartsByteSize, a9 as validateSandboxMentionPath } from '../parts-IB-Kbb7z.js';
3
3
  import { ChatTurnIdentity, ChatTurnProducer } from '@tangle-network/agent-runtime';
4
4
  import { InteractionAnswerRoute, InteractionAnswerRouteOptions } from '../interactions/index.js';
5
- import { PersistedChatMessageForTurn, TurnEventStore } from '../stream/index.js';
6
- export { C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, a as FileIndexCache, b as FileIndexReadyResponse, c as FileIndexResponse, d as FileIndexWarmingResponse, S as SandboxFileTreeSource, e as SandboxTreeFile, f as SandboxTreeResult, g as createSandboxFileIndexRoute } from '../file-index-Bn6sitKb.js';
5
+ import { PersistedChatMessageForTurn } from '../stream/index.js';
6
+ import { d as TurnEventStore } from '../turn-buffer-C9mEgoop.js';
7
+ export { D as DEFAULT_STALE_TURN_LOCK_GRACE_MS, a as DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, R as ReconcileStaleTurnLockOptions, b as ReconcileStaleTurnLockResult, S as StaleTurnLockSandboxProbeResult, c as StaleTurnLockSessionProbeResult, r as reconcileStaleTurnLock } from '../stale-turn-lock-C8Na1cFZ.js';
8
+ import { J as JsonRecord } from '../stream-normalizer-DWvtmY6F.js';
9
+ export { C as CreateSandboxFileIndexRouteOptions, F as FileIndexAuthorization, a as FileIndexCache, b as FileIndexReadyResponse, c as FileIndexResponse, d as FileIndexWarmingResponse, S as SandboxFileTreeSource, e as SandboxTreeFile, f as SandboxTreeResult, g as createSandboxFileIndexRoute } from '../file-index-b26ee-_R.js';
10
+ import { SandboxExecChannel, PromptInputPart } from '../sandbox/index.js';
7
11
  import '@tangle-network/agent-interface';
8
12
  import '../contract-KfqJh_au.js';
9
13
  import '../plans/index.js';
14
+ import '@tangle-network/sandbox';
15
+ import '../auth-DuptSkWh.js';
16
+ import '../types-BEOvc_ue.js';
17
+ import '../harness/index.js';
18
+ import '../model-dF2h4xT9.js';
10
19
 
11
20
  /**
12
21
  * `createChatTurnRoutes` — the assembled server chat vertical (issue #188
@@ -40,10 +49,11 @@ import '../plans/index.js';
40
49
  * `handleChatTurn` stays the engine — the seams only wrap its input, its
41
50
  * producer stream, and its settle.
42
51
  *
43
- * Seam stability: `lifecycle` and `heartbeat` are generic and stable.
44
- * `turnLock`, `contextGate`, `beforeTurn`, and `onRawEvent` are `@experimental`
45
- * proven by a single consumer (gtm's chat vertical, #200) and may change once
46
- * a second consumer exercises them. They stay FLAT top-level options (not
52
+ * Seam stability: `lifecycle`, `heartbeat`, and `turnLock` are generic and
53
+ * stable (`turnLock` graduated with `/turn-stream`'s shared DO adapter, #221).
54
+ * `contextGate`, `beforeTurn`, and `onRawEvent` are `@experimental` proven
55
+ * by a single consumer (gtm's chat vertical, #200) and may change once a
56
+ * second consumer exercises them. They stay FLAT top-level options (not
47
57
  * grouped under a `hooks` object): that grouping would break the shipped
48
58
  * consumer's call for no mechanism gain, and this package's exports are
49
59
  * additive-only.
@@ -222,7 +232,8 @@ interface CreateChatTurnRoutesOptions<TContext = void> {
222
232
  authorize(args: ChatTurnAuthorizeArgs): Promise<ChatTurnAuthorization<TContext>>;
223
233
  /** Thread/message persistence (`/chat-store`'s store or a product adapter). */
224
234
  store: ChatTurnMessageStore;
225
- /** Turn-event buffer (`createD1TurnEventStore(env.DB)` in production,
235
+ /** Turn-event buffer (`createD1TurnEventStore(env.DB)` or `/turn-stream`'s
236
+ * `createDurableObjectTurnEventStore(env.TURN_STREAM_DO)` in production,
226
237
  * `createMemoryTurnEventStore()` in tests). Wired by default — every turn
227
238
  * is buffered and replayable. */
228
239
  turnStore: TurnEventStore;
@@ -231,8 +242,9 @@ interface CreateChatTurnRoutesOptions<TContext = void> {
231
242
  * product's own producer. May be async (box resolution). */
232
243
  produce(args: ChatTurnProduceArgs<TContext>): ChatTurnRouteProducer | Promise<ChatTurnRouteProducer>;
233
244
  /** Single-flight lock acquired before any side effect and released once when
234
- * the turn settles (including short-circuit/throw). Omit → no lock.
235
- * @experimental Single-consumer (gtm, #200); shape may change. */
245
+ * the turn settles (including short-circuit/throw). `/turn-stream`'s
246
+ * `createDurableTurnLock` is the shared DO-backed implementation. Omit →
247
+ * no lock. */
236
248
  turnLock?: ChatTurnLock<TContext>;
237
249
  /** Pre-turn readiness gate that can short-circuit with a product `Response`
238
250
  * before the producer runs (the user row is already persisted). Runs after
@@ -318,154 +330,6 @@ interface ChatTurnRoutes {
318
330
  }
319
331
  declare function createChatTurnRoutes<TContext = void>(options: CreateChatTurnRoutesOptions<TContext>): ChatTurnRoutes;
320
332
 
321
- /**
322
- * Recovery policy for a `ChatTurnLock` whose holder died.
323
- *
324
- * `createChatTurnRoutes` takes the lock as a seam (`acquire`/`release`) and a
325
- * lock is a single-flight guard: while it is held, a second turn on the same
326
- * scope is refused. Products give it a TTL measured in tens of minutes, so a
327
- * turn that dies without releasing wedges chat for that whole window. Every
328
- * app on the seam inherits that wedge, which is why the way OUT of it is
329
- * policy this package owns rather than something each app rediscovers.
330
- *
331
- * The policy takes PROBES, not clients: it imports no sandbox SDK, opens no
332
- * connection, and knows nothing about how a product finds its box or talks to
333
- * a sidecar. That is what makes the rules testable and what keeps the concrete
334
- * probes — which box key, which session id, which sidecar endpoint — in the
335
- * product.
336
- *
337
- * The rules, in precedence order:
338
- *
339
- * 1. The session probe answered and the execution is TERMINAL ⇒ release, once
340
- * the lock is past a short grace period. The authority on "is this turn
341
- * still running" is whatever is actually running it; a terminal verdict is
342
- * proof the lock outlived its turn — but only if the verdict is about THIS
343
- * turn, which is what the grace buys (see
344
- * {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS}).
345
- * 2. The session probe answered and the execution is LIVE ⇒ hold, always.
346
- * Nothing below may override this. The lock is doing exactly its job.
347
- * 3. The probes could not reach that authority at all — the sandbox could not
348
- * be listed, is gone, is not running, or its session probe failed ⇒ fall
349
- * back on the physical argument: an execution runs INSIDE the box, so a box
350
- * that is not there is running nothing, and the lock is releasable. Without
351
- * this fallback the recovery would depend on the very subsystem whose
352
- * failure produced the stale lock.
353
- *
354
- * Rule 3 is gated on a grace period because it is an inference, not an
355
- * observation — see {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}.
356
- */
357
- /** Where the box is, as far as the caller can see. `state` on `not-running`
358
- * is the platform's own status string, carried through for the log. */
359
- type StaleTurnLockSandboxProbeResult = {
360
- status: 'running';
361
- } | {
362
- status: 'absent';
363
- } | {
364
- status: 'not-running';
365
- state?: string;
366
- };
367
- /** What the thing running the turn says about it. `terminal: false` means an
368
- * execution is LIVE — the strongest signal in the policy. `diagnostics` rides
369
- * through to the result and the logs unread. */
370
- type StaleTurnLockSessionProbeResult = {
371
- reachable: true;
372
- terminal: boolean;
373
- diagnostics?: Record<string, unknown>;
374
- } | {
375
- reachable: false;
376
- reason?: string;
377
- };
378
- /**
379
- * Minimum age a lock must reach before the "sandbox unreachable ⇒ nothing can
380
- * be running" fallback may force-release it.
381
- *
382
- * The lock is acquired BEFORE the box is ensured, so during a cold workspace's
383
- * first turn there is a real window in which the lock is held and no box exists
384
- * yet — indistinguishable, from a peek, from a box that vanished. The grace
385
- * period has to outlast that window (create + bootstrap + whatever the product
386
- * hydrates) or a concurrent request steals the lock from a turn that is merely
387
- * still provisioning. Five minutes clears observed cold starts with room to
388
- * spare while cutting the worst case from a TTL-length wedge down to five
389
- * minutes. Raising it makes recovery slower; lowering it risks stealing a lock
390
- * mid-provision.
391
- */
392
- declare const DEFAULT_STALE_TURN_LOCK_GRACE_MS: number;
393
- /**
394
- * Minimum age a lock must reach before a TERMINAL session verdict may release
395
- * it.
396
- *
397
- * The session probe is keyed on the THREAD, not on the execution the lock
398
- * holds: a sidecar that has nothing running reports `terminal` with
399
- * `activeExecutionId: null`, so there is no id to match the lock against. The
400
- * lock, meanwhile, is acquired BEFORE the box is ensured and before the
401
- * execution registers with the sidecar. Between those two moments a second
402
- * request that reconciles the lock asks the sidecar about a turn it has not
403
- * heard of yet and gets back the PREVIOUS turn's terminal state — proof about
404
- * the wrong execution. Releasing on that verdict hands the second request a
405
- * lock the first one is still using, which is two concurrent turns on a scope
406
- * whose single-flight guard just voted for itself.
407
- *
408
- * One minute covers the acquire → box-ensure → sidecar-registration window on
409
- * a warm box (the cold-box case is Rule 3's, and has its own, much longer
410
- * grace). Deliberately NOT
411
- * {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS}: this branch has a positive
412
- * observation behind it, so it should recover fast, and stretching it to five
413
- * minutes would leave a genuinely dead turn wedged for the whole window that
414
- * the session probe exists to shortcut. Raising it delays recovery from a
415
- * crashed turn; lowering it narrows the registration window it protects.
416
- */
417
- declare const DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS: number;
418
- interface ReconcileStaleTurnLockOptions {
419
- /** When the held lock was acquired (epoch ms). The grace period is measured
420
- * from here, so it must be the LOCK's start, not the turn's. */
421
- lockStartedAt: number;
422
- /** Is the box there and running? Never provisions — a peek, not an ensure.
423
- * A throw is treated as unreachable, same as `absent`. */
424
- probeSandbox(): Promise<StaleTurnLockSandboxProbeResult>;
425
- /** Ask the running box whether the execution is still live. Only called when
426
- * `probeSandbox` reported `running`. A throw is treated as unreachable. */
427
- probeSession(): Promise<StaleTurnLockSessionProbeResult>;
428
- /** Release the lock, fenced by the instant the releasing evidence was
429
- * observed. `fence.observedAt` is snapshotted BEFORE the probe that
430
- * justified the release, so a store that can compare it against the held
431
- * lock's start refuses to delete a SUCCESSOR lock acquired while the probe
432
- * was in flight. A store that cannot make that comparison may ignore the
433
- * fence, but must not substitute its own `Date.now()` — that timestamp is
434
- * by construction newer than any successor and makes the check vacuous.
435
- *
436
- * Returns whether the release actually landed — `false` when the lock was
437
- * already gone (someone else got there first), which is reported, never
438
- * treated as a release. */
439
- release(fence: {
440
- observedAt: number;
441
- }): boolean | Promise<boolean>;
442
- /** Override {@link DEFAULT_STALE_TURN_LOCK_GRACE_MS} (Rule 3's fallback). */
443
- graceMs?: number;
444
- /** Override {@link DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS} (Rule 1's release). */
445
- terminalGraceMs?: number;
446
- /** Identity fields merged into every log line (workspace, thread, execution
447
- * id — whatever makes the entry findable in the product's logs). */
448
- context?: Record<string, unknown>;
449
- /** Defaults to `console.warn`. Both the withheld and the force-released
450
- * branches log; a force-release is never silent. */
451
- log?(message: string, meta: Record<string, unknown>): void;
452
- /** Injectable clock, for tests. */
453
- now?(): number;
454
- }
455
- interface ReconcileStaleTurnLockResult {
456
- released: boolean;
457
- /** Why the policy decided what it did — the probe's own diagnostics on the
458
- * reachable path, the unreachable reason and lock age on the fallback. */
459
- diagnostics: Record<string, unknown>;
460
- }
461
- /**
462
- * Decide whether a held lock is stale and, if so, release it.
463
- *
464
- * Never provisions and never mutates anything but the lock: a reconciliation
465
- * attempt on a cold workspace leaves it cold.
466
- */
467
- declare function reconcileStaleTurnLock(options: ReconcileStaleTurnLockOptions): Promise<ReconcileStaleTurnLockResult>;
468
-
469
333
  /**
470
334
  * Sandbox lane: bridge a raw sandbox event stream (`streamSandboxPrompt`) into
471
335
  * the `ChatTurnProducer` shape agent-runtime's `handleChatTurn` consumes AND
@@ -481,6 +345,30 @@ declare function reconcileStaleTurnLock(options: ReconcileStaleTurnLockOptions):
481
345
  * `step-finish` parts. `createChatTurnRoutes` reads both after drain.
482
346
  */
483
347
 
348
+ /** Outcome of a `promoteFilePart` attempt. `key`, when given, becomes the
349
+ * persisted part's row key (e.g. `attachment:<path>`) so repeat promotions
350
+ * of the same underlying file fold into one segment instead of appending;
351
+ * omitted, the default `getPartKey` keying applies.
352
+ *
353
+ * On failure, `part` is an OPTIONAL substitute part to persist in place of
354
+ * the raw url-bearing one — this is how a product swaps in a transcript
355
+ * notice (gtm persists a `warning` notice part, never the transient url) for
356
+ * a failed promotion instead of baking a `data:`/sandbox-path url into the
357
+ * durable row. When `part` is present it is persisted (via the same
358
+ * `recordPersistedPart` path as a success, honoring the optional `key`);
359
+ * when absent, the existing raw-part fallback applies unchanged — so a
360
+ * caller that only returns `{ succeeded: false, reason }` keeps today's
361
+ * behavior verbatim. */
362
+ type FilePartPromotionOutcome = {
363
+ succeeded: true;
364
+ part: Record<string, unknown>;
365
+ key?: string;
366
+ } | {
367
+ succeeded: false;
368
+ reason: string;
369
+ part?: Record<string, unknown>;
370
+ key?: string;
371
+ };
484
372
  interface SandboxChatProducerOptions {
485
373
  /** The raw sandbox event stream (e.g. `streamSandboxPrompt(...)`). */
486
374
  events: AsyncIterable<unknown>;
@@ -494,6 +382,34 @@ interface SandboxChatProducerOptions {
494
382
  * session's sidecar connection). Without it, non-renderable asks are only
495
383
  * logged — the run stays blocked until the broker times out. */
496
384
  declineInteraction?: (id: string) => Promise<void>;
385
+ /** Opt-in eager promotion of harness-emitted `file` parts. Unset, a `file`
386
+ * part persists exactly as the harness sent it — a transient `url` (a
387
+ * `data:` URI or in-sandbox path) baked into the transcript, which is
388
+ * today's behavior and stays byte-identical if this is never wired. Set,
389
+ * EVERY `file` part (never `image`, never any other kind) is routed
390
+ * through this callback instead of `recordPersistedPart`'s default
391
+ * fallback — including a part with NEITHER `id` NOR `url` (gtm always
392
+ * attempts promotion; such a part simply fails "carries no url" and
393
+ * resolves through the same failure path as any other rejection, rather
394
+ * than being persisted raw and unpromoted) — so the product can durably
395
+ * write the bytes and swap in a path-bearing part before the raw url ever
396
+ * reaches the persisted transcript. Keyed per source-prefixed `id:<id>` /
397
+ * `url:<url>` (an `id` and a `url` sharing the same text must never collide
398
+ * onto one memo entry) and memoized by PROMISE (not result), so re-emitted
399
+ * snapshot events for the same part —
400
+ * the harness resends the whole part on every update, not just deltas —
401
+ * fold onto the one in-flight or settled attempt rather than promoting
402
+ * twice or racing two concurrent writes; a raw part with neither `id` nor
403
+ * `url` cannot be keyed, so it is invoked UN-memoized (once per event) —
404
+ * each occurrence is its own attempt. A rejecting promise is caught,
405
+ * logged via `log`, and treated as `succeeded: false`. On `succeeded:
406
+ * false` the outcome's optional `part` (a substitute — e.g. a warning
407
+ * notice — see {@link FilePartPromotionOutcome}) persists in its place when
408
+ * given; otherwise the raw part persists exactly as it does today — this
409
+ * seam only decides whether to call the promoter and what to do with its
410
+ * outcomes; the promotion mechanics (vault write, key derivation, notice
411
+ * construction) live in the caller's callback, not here. */
412
+ promoteFilePart?: (raw: JsonRecord) => Promise<FilePartPromotionOutcome>;
497
413
  log?: (message: string, meta?: Record<string, unknown>) => void;
498
414
  }
499
415
  declare function createSandboxChatProducer(options: SandboxChatProducerOptions): ChatTurnRouteProducer;
@@ -588,4 +504,335 @@ declare function sanitizeUploadFilename(name: string): string;
588
504
  declare function bytesToBase64(bytes: Uint8Array): string;
589
505
  declare function createUploadRoute(options: CreateUploadRouteOptions): (request: Request) => Promise<Response>;
590
506
 
591
- export { type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, DEFAULT_STALE_TURN_LOCK_GRACE_MS, DEFAULT_TERMINAL_TURN_LOCK_GRACE_MS, type ReconcileStaleTurnLockOptions, type ReconcileStaleTurnLockResult, type SandboxChatProducerOptions, type SandboxUploadSink, type StaleTurnLockSandboxProbeResult, type StaleTurnLockSessionProbeResult, UPLOAD_INLINE_MAX_BYTES, UPLOAD_MAX_FILE_BYTES, type UploadAuthorization, type UploadedChatFile, bytesToBase64, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, reconcileStaleTurnLock, sanitizeUploadFilename, withDurableChatProjection };
507
+ /**
508
+ * Storage seams for the chat-attachment vertical (`resolveChatAttachments`,
509
+ * `buildDispatchParts`, `promoteAgentFilePart`). Structural function ports in
510
+ * the same style as `upload.ts`'s `SandboxUploadSink`: REQUIRED injection, no
511
+ * default implementation — agent-app owns the size/budget/idempotency
512
+ * mechanism, the product owns where the bytes actually live.
513
+ *
514
+ * One reader, two callers: `resolveChatAttachments` reads only the
515
+ * authoritative `size` (never trusting the client-reported size), while
516
+ * `buildDispatchParts` reads the inline `content` (base64 or raw bytes) to
517
+ * build a `data:` URI. Both fit behind ONE `ReadAttachmentFn` so a product
518
+ * wires a single vault/object-store adapter, not two. gtm's KV vault (stores
519
+ * base64 bodies) and `/object-store`'s `ObjectStore` (hands back raw bytes)
520
+ * both satisfy this shape.
521
+ */
522
+ /**
523
+ * The result of reading one stored attachment. `ok:true` MUST carry the
524
+ * authoritative decoded byte `size` (the cap is meaningless against a
525
+ * client-controlled number) and, when the caller needs to inline the file,
526
+ * its content as `base64` and/or raw `bytes`. `mediaType` is the stored
527
+ * content type when the store knows it (used as the fallback when the wire
528
+ * part carried none). `ok:false` carries a human `reason` that SHOULD name the
529
+ * offending path — it is surfaced verbatim in the caller's typed outcome.
530
+ */
531
+ type AttachmentReadResult = {
532
+ ok: true;
533
+ /** Authoritative decoded byte length of the stored content. */
534
+ size: number;
535
+ /** Inline content as base64 — reused verbatim for a `data:` URI, so a
536
+ * store holding already-base64 bodies never decodes-and-re-encodes. */
537
+ base64?: string;
538
+ /** Inline content as raw bytes — base64-encoded by the caller when a
539
+ * `data:` URI is needed. Ignored when `base64` is present. */
540
+ bytes?: Uint8Array;
541
+ /** Stored content type, when the store tracks one. */
542
+ mediaType?: string;
543
+ } | {
544
+ ok: false;
545
+ reason: string;
546
+ };
547
+ /**
548
+ * Read one stored attachment for `scopeId` (the product's workspace/tenant
549
+ * key) at its store-relative `path`. Missing, deleted, or unreadable content
550
+ * MUST resolve to `{ ok: false, reason }`, never throw — a store failure is a
551
+ * per-attachment outcome the caller folds into its own typed result, not a
552
+ * turn-level exception.
553
+ */
554
+ type ReadAttachmentFn = (scopeId: string, path: string) => Promise<AttachmentReadResult>;
555
+ /** Outcome of persisting one attachment. Mirrors `AttachmentReadResult`'s
556
+ * `ok`/`reason` shape and `upload.ts`'s `{ ok }` convention. */
557
+ type AttachmentWriteResult = {
558
+ ok: true;
559
+ } | {
560
+ ok: false;
561
+ reason: string;
562
+ };
563
+ /**
564
+ * Persist `content` for `scopeId` at `path`. `content` is either raw `bytes`
565
+ * or a base64 `string` — a string argument is ALWAYS base64 (never utf8), so
566
+ * a store that speaks base64 (gtm's vault) writes it verbatim and one that
567
+ * speaks bytes decodes once. Like the reader, failures resolve to
568
+ * `{ ok: false, reason }` rather than throwing.
569
+ *
570
+ * `opts` mirrors the vault frontmatter gtm's `writeAttachmentVaultFile`
571
+ * persists alongside the body (promote-file-parts.ts:181-190), so a product
572
+ * reimplementing that vault writer through this seam can reproduce it
573
+ * exactly:
574
+ * - `mediaType` — the resolved MIME type; gtm's frontmatter key `mime`.
575
+ * - `name` — the sanitized (store-path-safe) display filename; gtm passes
576
+ * this only to shape its oversize message, not into frontmatter.
577
+ * - `originalName` — the filename as the harness/browser reported it, BEFORE
578
+ * sanitization (`raw.filename ?? filename` — falls back to the sanitized
579
+ * name when the source carried none); gtm's frontmatter key `originalName`.
580
+ * This is the one field with no other recovery path once sanitization has
581
+ * run, so it must ride the write, not be re-derived after the fact.
582
+ * - `size` — the authoritative decoded byte length being written; gtm's
583
+ * frontmatter key `size`.
584
+ */
585
+ type WriteAttachmentFn = (scopeId: string, path: string, content: Uint8Array | string, opts: {
586
+ mediaType?: string;
587
+ name?: string;
588
+ originalName?: string;
589
+ size?: number;
590
+ }) => Promise<AttachmentWriteResult>;
591
+
592
+ /**
593
+ * `resolveChatAttachments` — validate a turn body's `attachments` field into
594
+ * persistable {@link ChatAttachmentPart}s. Every path is re-validated (a path
595
+ * off the wire is never trusted to stay inside the store root) and every size
596
+ * is re-derived from the STORED body via the injected {@link ReadAttachmentFn},
597
+ * never the client-reported `size` — the upload path lets a caller rewrite its
598
+ * own frontmatter, so a stored size cannot bound anything and the wire size can
599
+ * be anything. Both the aggregate cap and the size carried on the returned part
600
+ * come from the authoritative read.
601
+ *
602
+ * Storage-parameterized: the frontmatter parsing / base64 sizing that derives
603
+ * the authoritative size lives BEHIND `readAttachment` (a product's vault or
604
+ * object-store adapter), so this module is a pure validator + budget gate with
605
+ * no store knowledge. Lifted from gtm-agent's `resolve-attachments.ts`
606
+ * (workspaceId → scopeId, the vault read → the injected reader) and kept
607
+ * behavior-identical for gtm-agent#618 adoption.
608
+ */
609
+
610
+ type ResolveChatAttachmentsResult = {
611
+ succeeded: true;
612
+ value: ChatAttachmentPart[];
613
+ } | {
614
+ succeeded: false;
615
+ error: string;
616
+ };
617
+ /** Verdict of a path check: OK, or a rejection naming why. Mirrors
618
+ * `SandboxMentionPathCheck` in `./wire`. */
619
+ type AttachmentPathCheck = {
620
+ succeeded: true;
621
+ } | {
622
+ succeeded: false;
623
+ error: string;
624
+ };
625
+ /** Most files a single request may carry. */
626
+ declare const ATTACHMENT_MAX_COUNT = 10;
627
+ /** Aggregate raw-byte ceiling across one message's attachments. */
628
+ declare const MAX_ATTACHMENT_TOTAL_BYTES: number;
629
+ /** Human-readable error for a message whose combined attachments exceed the
630
+ * aggregate raw-byte ceiling. Ported to match gtm's `attachmentTotalSizeErrorMessage`
631
+ * (attachment-limits.ts:93-95) verbatim, via the shared {@link formatBytes} —
632
+ * e.g. "Attachments total 25MB; each message is limited to 25MB", not raw
633
+ * byte counts. */
634
+ declare function attachmentTotalSizeErrorMessage(totalBytes: number, limitBytes: number): string;
635
+ /**
636
+ * Default path validator when a caller supplies none. Rejects the ways a path
637
+ * picked in a client can escape the store root — traversal (`..` segment),
638
+ * absolute (leading `/`), backslashes, null bytes, control characters (see
639
+ * {@link CONTROL_CHARS} — a path also feeds {@link buildAttachmentPromptBlock}'s
640
+ * `(vault: ${path})` pointer, so it is exposed to the same injection surface as
641
+ * `name`) — plus a dotfile/hidden segment (a leading `.` on any segment).
642
+ * Generalized from gtm's `validateVaultFilePath`, in the spirit of
643
+ * `validateSandboxMentionPath` (`/chat-routes`'s wire mention-path validator) —
644
+ * but the dotfile rejection here is INTENTIONALLY stricter than that sibling:
645
+ * an uploaded attachment path is sanitized store-relative storage the product
646
+ * itself assigned, whereas a mention path points at a file that already exists
647
+ * in the sandbox and may legitimately live under a dotfile segment. A caller
648
+ * that needs gtm's exact (looser) rule can supply `validatePath` to override
649
+ * this default entirely.
650
+ */
651
+ declare function defaultValidateAttachmentPath(path: string): AttachmentPathCheck;
652
+ interface ResolveChatAttachmentsOptions {
653
+ /** The product's workspace/tenant key, passed to `readAttachment`. */
654
+ scopeId: string;
655
+ /** Authoritative size + content reader — see {@link ReadAttachmentFn}. */
656
+ readAttachment: ReadAttachmentFn;
657
+ /** Most attachments one request may carry. Default {@link ATTACHMENT_MAX_COUNT}. */
658
+ maxCount?: number;
659
+ /** Aggregate raw-byte ceiling. Default {@link MAX_ATTACHMENT_TOTAL_BYTES}. */
660
+ maxTotalBytes?: number;
661
+ /** Path validator override. Default {@link defaultValidateAttachmentPath}. */
662
+ validatePath?: (path: string) => AttachmentPathCheck;
663
+ }
664
+ /**
665
+ * Validate and resolve a turn body's `attachments` field into persistable
666
+ * parts. Every path is confirmed present (and not deleted) in the caller's own
667
+ * store by `readAttachment` before it is trusted, and size is derived from the
668
+ * authoritative read for both the aggregate cap and the returned part's size.
669
+ */
670
+ declare function resolveChatAttachments(value: unknown, options: ResolveChatAttachmentsOptions): Promise<ResolveChatAttachmentsResult>;
671
+
672
+ /**
673
+ * `buildDispatchParts` — assemble the `PromptInputPart[]` a turn carrying
674
+ * attachments and/or `@`-mentions dispatches to the sandbox. `parts[0]` is
675
+ * always the full prompt text (typed text plus the attachment + mention pointer
676
+ * blocks); each attachment or mention becomes one media part. An attachment
677
+ * (read from the product store via the injected reader) draws the inline byte
678
+ * budget first; a mention (read from the LIVE box) takes what is left. A file
679
+ * inlines as a `data:` URI when it fits the remaining budget, otherwise demotes
680
+ * to an in-box path part so the whole request stays under the proxy cap. Every
681
+ * media part is deduped by its resolved absolute path. This module only
682
+ * produces the parts array; the caller decides when a turn dispatches parts
683
+ * instead of a plain string.
684
+ *
685
+ * Storage-parameterized port of gtm-agent's `dispatch-parts.ts`: the vault
686
+ * default reader is dropped (`readAttachment` is REQUIRED — the product supplies
687
+ * its store adapter), the `GTM_SANDBOX_VAULT_DIR` prefixing becomes the required
688
+ * `resolveAttachmentPath` seam, the `GTM_MULTIMODAL_FORCE_PATH` env fallback
689
+ * becomes an explicit `forcePath` flag, and every budget cap reads an overridable
690
+ * `./wire` constant. Kept behavior-identical for gtm-agent#618 adoption (the
691
+ * demotion math and emitted part shapes reproduce its dispatched prompt bytes).
692
+ */
693
+
694
+ type DispatchPartsOutcome = {
695
+ succeeded: true;
696
+ value: PromptInputPart[];
697
+ } | {
698
+ succeeded: false;
699
+ error: string;
700
+ };
701
+ /** One mention file's size (always) and inline bytes (only when the caller
702
+ * asked for them — a path-only mention never reads its bytes). */
703
+ type SandboxMentionReadOutcome = {
704
+ succeeded: true;
705
+ value: {
706
+ size: number;
707
+ base64?: string;
708
+ };
709
+ } | {
710
+ succeeded: false;
711
+ error: string;
712
+ };
713
+ type ReadSandboxMentionFn = (box: SandboxExecChannel, absolutePath: string, options: {
714
+ readBytes: boolean;
715
+ }) => Promise<SandboxMentionReadOutcome>;
716
+ interface BuildDispatchPartsInput {
717
+ text: string;
718
+ attachments: ChatAttachmentPart[];
719
+ mentions?: ChatMentionPart[];
720
+ history: Array<{
721
+ role: 'user' | 'assistant';
722
+ content: string;
723
+ }>;
724
+ systemPrompt: string;
725
+ /** Serialized size of the backend profile the SDK inlines into the same
726
+ * prompt request body — a large, non-negotiable rider that must come out of
727
+ * the inline budget or near-cap attachments 413 at the proxy instead of
728
+ * demoting to path parts. */
729
+ profileWireBytes: number;
730
+ /** The product's workspace/tenant key, passed to `readAttachment`. */
731
+ scopeId: string;
732
+ /** Maps an attachment's store-relative path to the in-box absolute path a
733
+ * path-based part references (same seam style as `fileMentionsToParts`'s
734
+ * `resolvePath`). */
735
+ resolveAttachmentPath: (path: string) => string;
736
+ /** Maps a mention's workspace-relative path to its in-box absolute path.
737
+ * Default: {@link BuildDispatchPartsInput.resolveAttachmentPath} — in gtm the
738
+ * vault mount roots both; a product that mounts them apart overrides this. */
739
+ resolveMentionPath?: (path: string) => string;
740
+ /** The turn's already-ensured box — required when `mentions` is non-empty
741
+ * (mention bytes are read from the live box, not the store). */
742
+ box?: SandboxExecChannel;
743
+ /** Force every media part to a path reference, skipping all inlining. */
744
+ forcePath?: boolean;
745
+ /** REQUIRED store reader for attachment content — no default (the product
746
+ * owns its store; see {@link ReadAttachmentFn}). */
747
+ readAttachment: ReadAttachmentFn;
748
+ readSandboxMention?: ReadSandboxMentionFn;
749
+ /** Whole-request proxy cap. Default {@link DISPATCH_REQUEST_MAX_BYTES}. */
750
+ requestMaxBytes?: number;
751
+ /** JSON-envelope reserve off the top of the request cap. Default
752
+ * {@link DISPATCH_STRUCTURAL_RESERVE_BYTES}. */
753
+ structuralReserveBytes?: number;
754
+ /** Sidecar per-request parts-array cap. Default {@link DISPATCH_MAX_PARTS}. */
755
+ maxParts?: number;
756
+ }
757
+ declare function buildDispatchParts(input: BuildDispatchPartsInput): Promise<DispatchPartsOutcome>;
758
+
759
+ /**
760
+ * `promoteAgentFilePart` — turn a harness-emitted `type:"file"` stream part
761
+ * into a store-backed {@link ChatAttachmentPart}. The harness hands back a URL
762
+ * pointing at bytes it produced (a `data:` URI, or a path inside the sandbox);
763
+ * nothing durable survives past the turn unless it is written into the
764
+ * product's store, the same way a user upload is. Typed outcomes throughout:
765
+ * every failure mode (unsupported scheme, no sandbox, oversize, store-write
766
+ * failure, malformed part) resolves to `{ succeeded: false, filename, reason }`
767
+ * rather than throwing past this boundary, so the caller folds a visible notice
768
+ * instead of losing the file silently.
769
+ *
770
+ * Storage-parameterized port of gtm-agent's `promote-file-parts.ts` with the
771
+ * refactor gtm never made: persistence goes through the injected
772
+ * {@link WriteAttachmentFn} (gtm hard-wired its vault writer), the path strategy
773
+ * is the injected `buildAttachmentPath` (neutral `uploads/agent/<date>/` default,
774
+ * no domain bucket taxonomy baked), the MIME map is an injectable hook, and the
775
+ * date segment reads an injectable clock. The idempotent `hash8(id ?? url ??
776
+ * filename)` naming is preserved so re-promoting the same source part resolves
777
+ * to the same path.
778
+ */
779
+
780
+ /** Default ceiling on a promoted file's raw (pre-encoding) byte size. */
781
+ declare const PROMOTE_MAX_FILE_BYTES: number;
782
+ interface RawAgentFilePart {
783
+ type: 'file';
784
+ id?: string;
785
+ filename?: string;
786
+ /** AI-SDK-shaped parts carry the MIME type here… */
787
+ mediaType?: string;
788
+ /** …but OpenCode's native FilePart calls the same field `mime`. */
789
+ mime?: string;
790
+ url?: string;
791
+ }
792
+ type PromoteFilePartResult = {
793
+ succeeded: true;
794
+ part: ChatAttachmentPart;
795
+ } | {
796
+ succeeded: false;
797
+ filename: string;
798
+ reason: string;
799
+ };
800
+ /** Arguments handed to a {@link PromoteAgentFilePartOptions.buildAttachmentPath}
801
+ * override — everything needed to place the file deterministically. */
802
+ interface AttachmentPathArgs {
803
+ /** Sanitized display filename (basename, safe charset). */
804
+ filename: string;
805
+ /** First 8 hex chars of the SHA-256 idempotency digest. */
806
+ hash8: string;
807
+ /** `YYYY-MM-DD` from the injected clock. */
808
+ date: string;
809
+ /** Resolved media type. */
810
+ mediaType: string;
811
+ /** `image`/`file` split of the media type. */
812
+ kind: ChatAttachmentKind;
813
+ }
814
+ /** Default MIME hook: extension → mime, or `text/plain` for the unknown. */
815
+ declare function sniffMimeFromName(filename: string): string;
816
+ interface PromoteAgentFilePartOptions {
817
+ raw: RawAgentFilePart;
818
+ /** The turn's box — required only to promote a sandbox-path part; a `data:`
819
+ * URI needs none. */
820
+ box?: SandboxExecChannel;
821
+ /** The product's workspace/tenant key, passed to `writeAttachment`. */
822
+ scopeId: string;
823
+ /** The turn's session id, used for the sandbox stat/read exec calls. */
824
+ sessionId: string;
825
+ /** REQUIRED store writer — no default (the product owns its store). */
826
+ writeAttachment: WriteAttachmentFn;
827
+ /** Store-path strategy. Default {@link defaultBuildAttachmentPath}. */
828
+ buildAttachmentPath?: (args: AttachmentPathArgs) => string;
829
+ /** Raw-byte ceiling. Default {@link PROMOTE_MAX_FILE_BYTES}. */
830
+ maxBytes?: number;
831
+ /** Last-resort media-type hook. Default {@link sniffMimeFromName}. */
832
+ sniffMime?: (filename: string) => string;
833
+ /** Clock for the date path segment. Default `() => new Date()`. */
834
+ now?: () => Date;
835
+ }
836
+ declare function promoteAgentFilePart(options: PromoteAgentFilePartOptions): Promise<PromoteFilePartResult>;
837
+
838
+ export { ATTACHMENT_MAX_COUNT, type AttachmentPathArgs, type AttachmentPathCheck, type AttachmentReadResult, type AttachmentWriteResult, type BuildDispatchPartsInput, ChatAttachmentKind, type ChatRouteDurableProjection, type ChatRouteDurableProjectionLogger, type ChatTurnAuthorization, type ChatTurnAuthorizeArgs, ChatTurnFilePartInput, type ChatTurnGateResult, type ChatTurnHeartbeat, type ChatTurnInputPatch, type ChatTurnLifecycle, type ChatTurnLifecycleComplete, type ChatTurnLifecycleError, type ChatTurnLifecycleStart, type ChatTurnLock, type ChatTurnLockResult, type ChatTurnMessageStore, ChatTurnPartInput, type ChatTurnProduceArgs, ChatTurnRequestPayload, type ChatTurnRouteProducer, type ChatTurnRoutes, type ChatTurnUsage, type CreateChatTurnRoutesOptions, type CreateUploadRouteOptions, type DispatchPartsOutcome, type FilePartPromotionOutcome, MAX_ATTACHMENT_TOTAL_BYTES, 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, attachmentTotalSizeErrorMessage, buildDispatchParts, bytesToBase64, createChatTurnRoutes, createSandboxChatProducer, createUploadRoute, defaultValidateAttachmentPath, promoteAgentFilePart, resolveChatAttachments, sanitizeUploadFilename, sniffMimeFromName, withDurableChatProjection };