@deepseek-ai/dsh-subagent 0.1.2-alpha.2 → 0.1.2-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md
5
- README.md: 76df70351d711d580d3ab1a89d0f929b85eab172
6
- README.zh.md: c4deb001c47435d29a5ca18cc0f8c0d26e48941e
5
+ README.md: f13de661015f3aa59b776273376f07653f6bbba5
6
+ README.zh.md: 42923504ed11b8eff7c0bdcd9d0c9505d741f42e
package/README.md CHANGED
@@ -48,7 +48,7 @@ One-shot children run once and settle with a single result, plus an optional str
48
48
 
49
49
  ### Following up, interrupting, and discovering
50
50
 
51
- Continuable children answer follow-up messages as their next turns, and the parent can interrupt a running turn or list its children at any time. Discovery covers both shapes: the service lists direct children and the full descendant tree — mode, activity, and lineage — reading live session state and optional persistence, without loading any child.
51
+ Continuable children answer follow-up messages as their next turns, and the parent can interrupt a running turn or list its children at any time. A browser continuation prompt may carry image parts: the Host admits and persists each image batch through the attachment store before the child inbox accepts the message, and refuses delivery when the child's declared model does not accept image input. Discovery covers both shapes: the service lists direct children and the full descendant tree — mode, activity, and lineage — reading live session state and optional persistence, without loading any child.
52
52
 
53
53
  ### Failure and recovery
54
54
 
package/README.zh.md CHANGED
@@ -48,7 +48,7 @@ kind: "package-reference"
48
48
 
49
49
  ### 后续消息、中断与发现
50
50
 
51
- 可继续子 agent 把后续消息作为下一个轮次回答,父级随时可以中断运行中的轮次或列举自己的子级。发现覆盖两种形态:服务列举直接子级与完整后代树——模式、活动状态与血缘——直接读取在线会话状态与可选持久化,不加载任何子 agent。
51
+ 可继续子 agent 把后续消息作为下一个轮次回答,父级随时可以中断运行中的轮次或列举自己的子级。浏览器发出的继续执行 prompt 可以携带图片部分:Host 先通过附件存储完成整批图片的准入与持久化,子级 inbox 才接受这条消息;当子级声明的模型不接受图片输入时拒绝投递。发现覆盖两种形态:服务列举直接子级与完整后代树——模式、活动状态与血缘——直接读取在线会话状态与可选持久化,不加载任何子 agent。
52
52
 
53
53
  ### 失败与恢复
54
54
 
package/lib/index.js CHANGED
@@ -1,9 +1,10 @@
1
+ import { AttachmentError, admitPromptContent } from "@deepseek-ai/dsh-attachment";
1
2
  import { scopeTarget } from "@deepseek-ai/dsh-scope";
2
3
  import { assertObjectJsonSchema } from "@deepseek-ai/dsh-tools";
3
4
  import { canonicalClientTimeZone } from "@deepseek-ai/dsh-util-time";
4
5
  import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
5
6
  import { z } from "zod";
6
- import { HarnessError, ReasoningEffortId, boundContextSummary, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
7
+ import { HarnessError, ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain } from "@deepseek-ai/dsh-llm";
7
8
  import { randomUUID } from "node:crypto";
8
9
  import { foldConsumedWork } from "@deepseek-ai/dsh-agent";
9
10
  import { brandString } from "@deepseek-ai/dsh-brand";
@@ -59,31 +60,6 @@ function validateControlRequest(method, payload) {
59
60
  if (!parsed.success) throw new RemoteError("gateway/bad-request", `invalid payload for ${method}`, { issues: parsed.error.issues });
60
61
  }
61
62
  /**
62
- * Admit the content one continuation may deliver, refusing every image.
63
- *
64
- * The blocks become the child's user message verbatim, and this surface admits
65
- * no attachment: nothing here registers encoded bytes with the attachment
66
- * service, so an image would reach the child as a reference nothing resolves.
67
- * The wire accepts the encoded upload so this refusal — not a Client that
68
- * strips the block — is what the caller is answered with. Other block types
69
- * still cross unnarrowed.
70
- * @param childSessionId - the addressed child, named by the refusal.
71
- * @param content - blocks the caller asked to deliver.
72
- * @returns the admitted blocks, in order, as the durable content vocabulary.
73
- * @throws {RemoteError} `subagent/attachment-unsupported` when any block is an image.
74
- */
75
- function admitPromptContent(childSessionId, content) {
76
- const admitted = [];
77
- for (const block of content) {
78
- if (block.type === "image") throw new RemoteError("subagent/attachment-unsupported", "subagent continuation does not accept images", {
79
- childSessionId,
80
- reason: "SUBAGENT_IMAGE_UNSUPPORTED"
81
- });
82
- admitted.push(block);
83
- }
84
- return admitted;
85
- }
86
- /**
87
63
  * Project one durable listing onto the catalog view, replacing each row's
88
64
  * store-derived activity with the live Agent driver's status and reporting
89
65
  * whether the exact parent Agent is live. Without an Agent registry no driver
@@ -128,7 +104,9 @@ function rejectCatalogRead(error, signal) {
128
104
  */
129
105
  function rejectPrompt(error, childSessionId, signal) {
130
106
  if (isCancellation(error, signal)) throw new RemoteError("gateway/cancelled", "subagent prompt was cancelled", {}, { cause: error });
107
+ if (error instanceof AttachmentError) throw new RemoteError("subagent/attachment-invalid", error.message, { reason: error.code }, { cause: error });
131
108
  if (error instanceof SubagentError) switch (error.code) {
109
+ case "MODEL_DOES_NOT_SUPPORT_IMAGES": throw new RemoteError("subagent/attachment-invalid", error.message, { reason: error.code }, { cause: error });
132
110
  case "NOT_RESUMABLE": throw new RemoteError("subagent/not-resumable", "subagent cannot be resumed", { childSessionId }, { cause: error });
133
111
  case "UNAUTHORIZED": throw new RemoteError("subagent/unauthorized", "subagent does not belong to this parent", { childSessionId }, { cause: error });
134
112
  case "DRAINING":
@@ -1070,11 +1048,19 @@ var SubagentContinuationManager = class {
1070
1048
  const live = await this.locks.run(childId, async () => {
1071
1049
  const activation = this.activations.get(childId);
1072
1050
  if (activation === void 0) return this.coldResume(parent, childId, content, options);
1051
+ const disposal = activation.disposal;
1073
1052
  /* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
1074
1053
  * delivery to observe the transaction inside the same critical section that opened it,
1075
1054
  * which no test can schedule deterministically. The behavior is covered end-to-end by
1076
1055
  * "cold-resumes a delivery that lost the race with final disposal". */
1077
- if (activation.disposal !== void 0) return activation.disposal.then(() => void 0, () => void 0);
1056
+ if (disposal !== void 0) return disposal.then(() => void 0, () => void 0);
1057
+ if (contentHasImage(content)) {
1058
+ await this.assertImageCapable(activation.handle.agent, options.signal);
1059
+ if (activation.disposal !== void 0) {
1060
+ await Promise.allSettled([activation.disposal]);
1061
+ return;
1062
+ }
1063
+ }
1078
1064
  return this.submitAdmitted(activation, content, options.source, parent, options.signal);
1079
1065
  });
1080
1066
  /* v8 ignore start -- only the lost-cutoff arm above returns undefined, so only that
@@ -1414,6 +1400,10 @@ var SubagentContinuationManager = class {
1414
1400
  */
1415
1401
  async submitMaterialized(activation, content, source, parent, signal) {
1416
1402
  try {
1403
+ if (contentHasImage(content)) {
1404
+ await this.assertImageCapable(activation.handle.agent, signal);
1405
+ if (activation.disposal !== void 0) throw new SubagentError(`subagent "${activation.childId}" is closing`, "ACTIVATION_CLOSING");
1406
+ }
1417
1407
  return this.submitAdmitted(activation, content, source, parent, signal);
1418
1408
  } catch (error) {
1419
1409
  /* v8 ignore next -- rollback disposal failures must not mask the
@@ -1423,6 +1413,28 @@ var SubagentContinuationManager = class {
1423
1413
  }
1424
1414
  }
1425
1415
  /**
1416
+ * Refuse image content addressed to a child whose model accepts text only.
1417
+ * Callers guard with `contentHasImage`, so text-only delivery never awaits.
1418
+ * The check runs inside the per-child delivery lock, before the message
1419
+ * exists, so a rejection leaves no partial user message. When the child's
1420
+ * route is not fixed by its options (a request-waterfall listener owns it)
1421
+ * or no LLM registry is composed, delivery proceeds and the LLM layer's
1422
+ * text-only projection replaces each image with its stable placeholder.
1423
+ * @param agent - the live or freshly materialized child agent.
1424
+ * @param signal - caller cancellation bounding the model-info read.
1425
+ * @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
1426
+ */
1427
+ async assertImageCapable(agent, signal) {
1428
+ const { provider, model } = agent.options;
1429
+ if (provider === void 0 || model === void 0) return;
1430
+ const llm = this.ctx.get("llm");
1431
+ /* v8 ignore next -- a deployment without the LLM registry serves no model
1432
+ * to refuse against; delivery then defers to the text-only projection. */
1433
+ if (llm === void 0) return;
1434
+ const info = await llm.resolveModelInfo(provider, model, signal);
1435
+ if (info.inputModalities !== void 0 && !info.inputModalities.includes("image")) throw new SubagentError(`Model "${model}" does not support image input.`, "MODEL_DOES_NOT_SUPPORT_IMAGES");
1436
+ }
1437
+ /**
1426
1438
  * Create or resume the child Agent through the private activation-owner
1427
1439
  * scope, install the handle in a fresh Activation, and register ownership on
1428
1440
  * a continuation-managed parent. Rejection leaves no Activation, no handle,
@@ -2971,10 +2983,12 @@ let SubagentRuntime = (() => {
2971
2983
  * validated browser zone on the accepted message. Success identifies the
2972
2984
  * message the child's FIFO inbox accepted; later execution is independent of
2973
2985
  * this call.
2986
+ * Image parts are admitted and persisted through the attachment store
2987
+ * before delivery, and the child's model must accept image input.
2974
2988
  * @param request - durable address, minted identity, content, and optional browser zone.
2975
2989
  * @param signal - carrier cancellation, owning the call until inbox acceptance.
2976
2990
  * @returns the accepted message's inbox identity.
2977
- * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-unsupported`,
2991
+ * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-invalid`,
2978
2992
  * `subagent/invalid-time-zone`, `subagent/parent-unavailable`,
2979
2993
  * `subagent/not-resumable`, `subagent/unauthorized`,
2980
2994
  * `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.
@@ -2982,7 +2996,6 @@ let SubagentRuntime = (() => {
2982
2996
  async prompt(request, signal) {
2983
2997
  const { parentSessionId, childSessionId, clientTimeZone } = request;
2984
2998
  validateControlRequest("subagent.prompt", request);
2985
- const content = admitPromptContent(childSessionId, request.content);
2986
2999
  const canonicalTimeZone = clientTimeZone === void 0 ? void 0 : canonicalClientTimeZone(clientTimeZone);
2987
3000
  if (clientTimeZone !== void 0 && canonicalTimeZone === void 0) throw new RemoteError("subagent/invalid-time-zone", "clientTimeZone must be UTC or a valid IANA Area/Location name", { value: clientTimeZone });
2988
3001
  const parent = this.ctx.get("agents")?.get(parentSessionId);
@@ -2993,6 +3006,16 @@ let SubagentRuntime = (() => {
2993
3006
  ...canonicalTimeZone === void 0 ? {} : { clientTimeZone: canonicalTimeZone }
2994
3007
  };
2995
3008
  try {
3009
+ let content;
3010
+ if (request.content.every((part) => part.type === "text")) content = request.content.map((part) => ({
3011
+ type: "text",
3012
+ text: part.text
3013
+ }));
3014
+ else {
3015
+ const attachments = this.ctx.get("attachments");
3016
+ if (attachments === void 0) throw new Error("subagent image prompt requires an attachment store");
3017
+ content = await admitPromptContent(attachments, request.content);
3018
+ }
2996
3019
  return { messageId: await this.followup(parent, childSessionId, content, {
2997
3020
  source,
2998
3021
  signal
@@ -1,37 +1,6 @@
1
1
  /* Generated by @deepseek-ai/dsh-typert-generator from FaceModel — do not edit. */
2
2
  import { z } from 'zod'
3
3
 
4
- const ToolResultBlockRemoteCodec$schema = z.object({
5
- 'type': z.literal("tool-result"),
6
- 'toolCallId': z.intersection(z.string(), z.unknown()),
7
- 'content': z.array(z.union([z.object({
8
- 'type': z.literal("text"),
9
- 'text': z.string(),
10
- }), z.object({
11
- 'type': z.literal("image"),
12
- 'attachment': z.object({
13
- 'attachmentId': z.intersection(z.string(), z.unknown()),
14
- 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]),
15
- 'bytes': z.number(),
16
- 'width': z.number(),
17
- 'height': z.number(),
18
- 'name': z.string().optional(),
19
- 'originalDimensions': z.object({
20
- 'width': z.number(),
21
- 'height': z.number(),
22
- }).optional(),
23
- }),
24
- }), z.object({
25
- 'type': z.literal("reasoning"),
26
- 'text': z.string(),
27
- }), z.object({
28
- 'type': z.literal("tool-call"),
29
- 'id': z.intersection(z.string(), z.unknown()),
30
- 'name': z.string(),
31
- 'arguments': z.string(),
32
- }), z.lazy(() => ToolResultBlockRemoteCodec$schema)])),
33
- 'isError': z.boolean().optional(),
34
- })
35
4
  const _deepseek_ai_dsh_subagent_subagents_interruptByParent_parameter_0$schema = z.intersection(z.string(), z.unknown())
36
5
  const _deepseek_ai_dsh_subagent_subagents_interruptByParent_parameter_1$schema = z.intersection(z.string(), z.unknown())
37
6
  const _deepseek_ai_dsh_subagent_subagents_interruptByParent_parameter_2$schema = z.literal("continuable")
@@ -69,65 +38,13 @@ const _deepseek_ai_dsh_subagent_subagents_prompt_parameter_0$schema = z.object({
69
38
  'childSessionId': z.intersection(z.string(), z.unknown()).readonly(),
70
39
  'mode': z.literal("continuable").readonly(),
71
40
  'content': z.array(z.union([z.object({
72
- 'type': z.literal("text"),
73
- 'text': z.string(),
74
- }), z.object({
75
- 'type': z.literal("image"),
76
- 'attachment': z.object({
77
- 'attachmentId': z.intersection(z.string(), z.unknown()),
78
- 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]),
79
- 'bytes': z.number(),
80
- 'width': z.number(),
81
- 'height': z.number(),
82
- 'name': z.string().optional(),
83
- 'originalDimensions': z.object({
84
- 'width': z.number(),
85
- 'height': z.number(),
86
- }).optional(),
87
- }),
88
- }), z.object({
89
- 'type': z.literal("reasoning"),
90
- 'text': z.string(),
91
- }), z.object({
92
- 'type': z.literal("tool-call"),
93
- 'id': z.intersection(z.string(), z.unknown()),
94
- 'name': z.string(),
95
- 'arguments': z.string(),
96
- }), z.object({
97
- 'type': z.literal("tool-result"),
98
- 'toolCallId': z.intersection(z.string(), z.unknown()),
99
- 'content': z.array(z.union([z.object({
100
- 'type': z.literal("text"),
101
- 'text': z.string(),
102
- }), z.object({
103
- 'type': z.literal("image"),
104
- 'attachment': z.object({
105
- 'attachmentId': z.intersection(z.string(), z.unknown()),
106
- 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]),
107
- 'bytes': z.number(),
108
- 'width': z.number(),
109
- 'height': z.number(),
110
- 'name': z.string().optional(),
111
- 'originalDimensions': z.object({
112
- 'width': z.number(),
113
- 'height': z.number(),
114
- }).optional(),
115
- }),
116
- }), z.object({
117
- 'type': z.literal("reasoning"),
118
- 'text': z.string(),
119
- }), z.object({
120
- 'type': z.literal("tool-call"),
121
- 'id': z.intersection(z.string(), z.unknown()),
122
- 'name': z.string(),
123
- 'arguments': z.string(),
124
- }), z.lazy(() => ToolResultBlockRemoteCodec$schema)])),
125
- 'isError': z.boolean().optional(),
41
+ 'type': z.literal("text").readonly(),
42
+ 'text': z.string().readonly(),
126
43
  }), z.object({
127
44
  'type': z.literal("image").readonly(),
128
- 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]),
129
- 'data': z.string(),
130
- 'name': z.string().optional(),
45
+ 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]).readonly(),
46
+ 'data': z.string().readonly(),
47
+ 'name': z.string().readonly().optional(),
131
48
  })])).readonly(),
132
49
  'clientTimeZone': z.string().readonly().optional(),
133
50
  })
@@ -184,7 +101,7 @@ export const TYPERT = {
184
101
  typeSymbol: '@deepseek-ai/dsh-subagent/client#SubagentInterruptReceipt',
185
102
  schema: _deepseek_ai_dsh_subagent_subagents_interruptByParent_result$schema,
186
103
  },
187
- sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":479,"column":3},
104
+ sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":491,"column":3},
188
105
  },
189
106
  {
190
107
  id: '@deepseek-ai/dsh-subagent#subagents/list',
@@ -211,7 +128,7 @@ export const TYPERT = {
211
128
  typeSymbol: '@deepseek-ai/dsh-subagent/client#SubagentCatalog',
212
129
  schema: _deepseek_ai_dsh_subagent_subagents_list_result$schema,
213
130
  },
214
- sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":406,"column":9},
131
+ sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":407,"column":9},
215
132
  },
216
133
  {
217
134
  id: '@deepseek-ai/dsh-subagent#subagents/prompt',
@@ -237,7 +154,7 @@ export const TYPERT = {
237
154
  typeSymbol: '@deepseek-ai/dsh-subagent/client#SubagentPromptReceipt',
238
155
  schema: _deepseek_ai_dsh_subagent_subagents_prompt_result$schema,
239
156
  },
240
- sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":430,"column":9},
157
+ sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":433,"column":9},
241
158
  },
242
159
  ],
243
160
  model: {
@@ -325,7 +242,7 @@ export const TYPERT = {
325
242
  "name": "prompt",
326
243
  "signature": "@Remote('prompt') async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt>",
327
244
  "summary": "Deliver one browser-authored message to a continuable child through the exact live direct parent, retaining the caller-minted request identity and validated browser zone on the accepted message.",
328
- "jsDoc": "/**\n * Deliver one browser-authored message to a continuable child through the\n * exact live direct parent, retaining the caller-minted request identity and\n * validated browser zone on the accepted message. Success identifies the\n * message the child's FIFO inbox accepted; later execution is independent of\n * this call.\n * @param request - durable address, minted identity, content, and optional browser zone.\n * @param signal - carrier cancellation, owning the call until inbox acceptance.\n * @returns the accepted message's inbox identity.\n * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-unsupported`,\n * `subagent/invalid-time-zone`, `subagent/parent-unavailable`,\n * `subagent/not-resumable`, `subagent/unauthorized`,\n * `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.\n */"
245
+ "jsDoc": "/**\n * Deliver one browser-authored message to a continuable child through the\n * exact live direct parent, retaining the caller-minted request identity and\n * validated browser zone on the accepted message. Success identifies the\n * message the child's FIFO inbox accepted; later execution is independent of\n * this call.\n * Image parts are admitted and persisted through the attachment store\n * before delivery, and the child's model must accept image input.\n * @param request - durable address, minted identity, content, and optional browser zone.\n * @param signal - carrier cancellation, owning the call until inbox acceptance.\n * @returns the accepted message's inbox identity.\n * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-invalid`,\n * `subagent/invalid-time-zone`, `subagent/parent-unavailable`,\n * `subagent/not-resumable`, `subagent/unauthorized`,\n * `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.\n */"
329
246
  },
330
247
  {
331
248
  "kind": "method",
@@ -476,14 +393,6 @@ export const TYPERT = {
476
393
  "name": "CoordinatorMessageSource",
477
394
  "declaration": "export interface CoordinatorMessageSource {\n readonly kind: 'coordinator';\n readonly form: 'relay';\n readonly senderSessionId: SessionId;\n}"
478
395
  },
479
- {
480
- "name": "EncodedImageAttachment",
481
- "declaration": "export interface EncodedImageAttachment {\n mediaType: ImageMediaType;\n data: string;\n name?: string;\n}"
482
- },
483
- {
484
- "name": "EncodedImagePromptBlock",
485
- "declaration": "export interface EncodedImagePromptBlock extends EncodedImageAttachment {\n readonly type: 'image';\n}"
486
- },
487
396
  {
488
397
  "name": "EpochHeader",
489
398
  "declaration": "export interface EpochHeader {\n config: LlmCallConfig;\n adapterDefaults?: LlmCallConfigAdapterDefaults;\n system?: string;\n tools?: ToolSchema[];\n}"
@@ -616,6 +525,10 @@ export const TYPERT = {
616
525
  "name": "OneShotSubagentDescriptorData",
617
526
  "declaration": "export interface OneShotSubagentDescriptorData extends SubagentDescriptorBase {\n readonly mode: 'one-shot';\n readonly label?: string;\n}"
618
527
  },
528
+ {
529
+ "name": "PromptContentPart",
530
+ "declaration": "export type PromptContentPart = { readonly type: 'text'; readonly text: string; } | { readonly type: 'image'; readonly mediaType: ImageMediaType; readonly data: string; readonly name?: string; };"
531
+ },
619
532
  {
620
533
  "name": "ProviderRequestId",
621
534
  "declaration": "export type ProviderRequestId = Branded<'ProviderRequestId'>;"
@@ -752,17 +665,13 @@ export const TYPERT = {
752
665
  "name": "SubagentListEntry",
753
666
  "declaration": "export type SubagentListEntry = { readonly kind: 'child'; readonly id: SessionId; readonly activity: 'running' | 'inactive'; readonly hasChildren: boolean; } & ({ readonly mode: 'one-shot'; readonly label?: string; } | { readonly mode: 'continuable'; readonly label: string; }) | { readonly kind: 'diagnostic'; readonly id: SessionId; readonly reason: 'corrupt' | 'unsupported' | 'unavailable'; };"
754
667
  },
755
- {
756
- "name": "SubagentPromptContentPart",
757
- "declaration": "export type SubagentPromptContentPart = ContentBlock | EncodedImagePromptBlock;"
758
- },
759
668
  {
760
669
  "name": "SubagentPromptReceipt",
761
670
  "declaration": "export interface SubagentPromptReceipt {\n readonly messageId: MessageId;\n}"
762
671
  },
763
672
  {
764
673
  "name": "SubagentPromptRequest",
765
- "declaration": "export interface SubagentPromptRequest {\n readonly requestId: SubagentPromptRequestId;\n readonly parentSessionId: SessionId;\n readonly childSessionId: SessionId;\n readonly mode: 'continuable';\n readonly content: readonly SubagentPromptContentPart[];\n readonly clientTimeZone?: string;\n}"
674
+ "declaration": "export interface SubagentPromptRequest {\n readonly requestId: SubagentPromptRequestId;\n readonly parentSessionId: SessionId;\n readonly childSessionId: SessionId;\n readonly mode: 'continuable';\n readonly content: readonly PromptContentPart[];\n readonly clientTimeZone?: string;\n}"
766
675
  },
767
676
  {
768
677
  "name": "SubagentPromptRequestId",
@@ -1,37 +1,6 @@
1
1
  /* Generated by @deepseek-ai/dsh-typert-generator from the Host FaceModel — do not edit. */
2
2
  import { z } from 'zod'
3
3
 
4
- const ToolResultBlockRemoteCodec$schema = z.object({
5
- 'type': z.literal("tool-result"),
6
- 'toolCallId': z.intersection(z.string(), z.unknown()),
7
- 'content': z.array(z.union([z.object({
8
- 'type': z.literal("text"),
9
- 'text': z.string(),
10
- }), z.object({
11
- 'type': z.literal("image"),
12
- 'attachment': z.object({
13
- 'attachmentId': z.intersection(z.string(), z.unknown()),
14
- 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]),
15
- 'bytes': z.number(),
16
- 'width': z.number(),
17
- 'height': z.number(),
18
- 'name': z.string().optional(),
19
- 'originalDimensions': z.object({
20
- 'width': z.number(),
21
- 'height': z.number(),
22
- }).optional(),
23
- }),
24
- }), z.object({
25
- 'type': z.literal("reasoning"),
26
- 'text': z.string(),
27
- }), z.object({
28
- 'type': z.literal("tool-call"),
29
- 'id': z.intersection(z.string(), z.unknown()),
30
- 'name': z.string(),
31
- 'arguments': z.string(),
32
- }), z.lazy(() => ToolResultBlockRemoteCodec$schema)])),
33
- 'isError': z.boolean().optional(),
34
- })
35
4
  const _deepseek_ai_dsh_subagent_subagents_interruptByParent_parameter_0$schema = z.intersection(z.string(), z.unknown())
36
5
  const _deepseek_ai_dsh_subagent_subagents_interruptByParent_parameter_1$schema = z.intersection(z.string(), z.unknown())
37
6
  const _deepseek_ai_dsh_subagent_subagents_interruptByParent_parameter_2$schema = z.literal("continuable")
@@ -69,65 +38,13 @@ const _deepseek_ai_dsh_subagent_subagents_prompt_parameter_0$schema = z.object({
69
38
  'childSessionId': z.intersection(z.string(), z.unknown()).readonly(),
70
39
  'mode': z.literal("continuable").readonly(),
71
40
  'content': z.array(z.union([z.object({
72
- 'type': z.literal("text"),
73
- 'text': z.string(),
74
- }), z.object({
75
- 'type': z.literal("image"),
76
- 'attachment': z.object({
77
- 'attachmentId': z.intersection(z.string(), z.unknown()),
78
- 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]),
79
- 'bytes': z.number(),
80
- 'width': z.number(),
81
- 'height': z.number(),
82
- 'name': z.string().optional(),
83
- 'originalDimensions': z.object({
84
- 'width': z.number(),
85
- 'height': z.number(),
86
- }).optional(),
87
- }),
88
- }), z.object({
89
- 'type': z.literal("reasoning"),
90
- 'text': z.string(),
91
- }), z.object({
92
- 'type': z.literal("tool-call"),
93
- 'id': z.intersection(z.string(), z.unknown()),
94
- 'name': z.string(),
95
- 'arguments': z.string(),
96
- }), z.object({
97
- 'type': z.literal("tool-result"),
98
- 'toolCallId': z.intersection(z.string(), z.unknown()),
99
- 'content': z.array(z.union([z.object({
100
- 'type': z.literal("text"),
101
- 'text': z.string(),
102
- }), z.object({
103
- 'type': z.literal("image"),
104
- 'attachment': z.object({
105
- 'attachmentId': z.intersection(z.string(), z.unknown()),
106
- 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]),
107
- 'bytes': z.number(),
108
- 'width': z.number(),
109
- 'height': z.number(),
110
- 'name': z.string().optional(),
111
- 'originalDimensions': z.object({
112
- 'width': z.number(),
113
- 'height': z.number(),
114
- }).optional(),
115
- }),
116
- }), z.object({
117
- 'type': z.literal("reasoning"),
118
- 'text': z.string(),
119
- }), z.object({
120
- 'type': z.literal("tool-call"),
121
- 'id': z.intersection(z.string(), z.unknown()),
122
- 'name': z.string(),
123
- 'arguments': z.string(),
124
- }), z.lazy(() => ToolResultBlockRemoteCodec$schema)])),
125
- 'isError': z.boolean().optional(),
41
+ 'type': z.literal("text").readonly(),
42
+ 'text': z.string().readonly(),
126
43
  }), z.object({
127
44
  'type': z.literal("image").readonly(),
128
- 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]),
129
- 'data': z.string(),
130
- 'name': z.string().optional(),
45
+ 'mediaType': z.union([z.literal("image/png"), z.literal("image/jpeg"), z.literal("image/webp"), z.literal("image/gif")]).readonly(),
46
+ 'data': z.string().readonly(),
47
+ 'name': z.string().readonly().optional(),
131
48
  })])).readonly(),
132
49
  'clientTimeZone': z.string().readonly().optional(),
133
50
  })
@@ -181,7 +98,7 @@ export const TYPERT_REMOTE = {
181
98
  typeSymbol: '@deepseek-ai/dsh-subagent/client#SubagentInterruptReceipt',
182
99
  schema: _deepseek_ai_dsh_subagent_subagents_interruptByParent_result$schema,
183
100
  },
184
- sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":479,"column":3},
101
+ sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":491,"column":3},
185
102
  },
186
103
  {
187
104
  id: '@deepseek-ai/dsh-subagent#subagents/list',
@@ -208,7 +125,7 @@ export const TYPERT_REMOTE = {
208
125
  typeSymbol: '@deepseek-ai/dsh-subagent/client#SubagentCatalog',
209
126
  schema: _deepseek_ai_dsh_subagent_subagents_list_result$schema,
210
127
  },
211
- sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":406,"column":9},
128
+ sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":407,"column":9},
212
129
  },
213
130
  {
214
131
  id: '@deepseek-ai/dsh-subagent#subagents/prompt',
@@ -234,7 +151,7 @@ export const TYPERT_REMOTE = {
234
151
  typeSymbol: '@deepseek-ai/dsh-subagent/client#SubagentPromptReceipt',
235
152
  schema: _deepseek_ai_dsh_subagent_subagents_prompt_result$schema,
236
153
  },
237
- sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":430,"column":9},
154
+ sourceLocation: {"file":"packages/subagent/subagent/src/index.ts","line":433,"column":9},
238
155
  },
239
156
  ],
240
157
  }
@@ -340,6 +340,19 @@ export declare class SubagentContinuationManager {
340
340
  * @returns the accepted inbox message id.
341
341
  */
342
342
  private submitMaterialized;
343
+ /**
344
+ * Refuse image content addressed to a child whose model accepts text only.
345
+ * Callers guard with `contentHasImage`, so text-only delivery never awaits.
346
+ * The check runs inside the per-child delivery lock, before the message
347
+ * exists, so a rejection leaves no partial user message. When the child's
348
+ * route is not fixed by its options (a request-waterfall listener owns it)
349
+ * or no LLM registry is composed, delivery proceeds and the LLM layer's
350
+ * text-only projection replaces each image with its stable placeholder.
351
+ * @param agent - the live or freshly materialized child agent.
352
+ * @param signal - caller cancellation bounding the model-info read.
353
+ * @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
354
+ */
355
+ private assertImageCapable;
343
356
  /**
344
357
  * Create or resume the child Agent through the private activation-owner
345
358
  * scope, install the handle in a fresh Activation, and register ownership on
@@ -74,7 +74,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
74
74
  });
75
75
  import { randomUUID } from 'node:crypto';
76
76
  import { brandString } from '@deepseek-ai/dsh-brand';
77
- import { ReasoningEffortId, boundContextSummary, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
77
+ import { ReasoningEffortId, boundContextSummary, contentHasImage, createUserMessage, errorChain } from '@deepseek-ai/dsh-llm';
78
78
  import { foldSubagentDescriptor, snapshotSubagentDescriptor } from "./descriptor.js";
79
79
  import { appendDelegatedPolicyOverrides, applyChildComposition, captureDelegatedPolicyOverrides, childSessionMeta, resolveChildAgentOptions, resolveChildDepth, } from "./child-agent.js";
80
80
  import { assertSubagentMaxDepth } from "./depth.js";
@@ -297,12 +297,25 @@ export class SubagentContinuationManager {
297
297
  return this.coldResume(parent, childId, content, options);
298
298
  // A delivery that arrives after the disposal transaction began must not
299
299
  // reach a handle being torn down; wait for release, then cold-resume.
300
+ const disposal = activation.disposal;
300
301
  /* v8 ignore next 3 -- the send-versus-dispose cutoff: reaching this arm needs a
301
302
  * delivery to observe the transaction inside the same critical section that opened it,
302
303
  * which no test can schedule deterministically. The behavior is covered end-to-end by
303
304
  * "cold-resumes a delivery that lost the race with final disposal". */
304
- if (activation.disposal !== undefined) {
305
- return activation.disposal.then(() => undefined, () => undefined);
305
+ if (disposal !== undefined) {
306
+ return disposal.then(() => undefined, () => undefined);
307
+ }
308
+ // Text-only delivery stays await-free, so the disposal-cutoff check
309
+ // above and the submit share one critical window. The image path
310
+ // awaits a capability read, so it re-checks the cutoff afterwards; a
311
+ // disposal that began during the read is waited out and retried like
312
+ // one observed on entry.
313
+ if (contentHasImage(content)) {
314
+ await this.assertImageCapable(activation.handle.agent, options.signal);
315
+ if (activation.disposal !== undefined) {
316
+ await Promise.allSettled([activation.disposal]);
317
+ return undefined;
318
+ }
306
319
  }
307
320
  return this.submitAdmitted(activation, content, options.source, parent, options.signal);
308
321
  });
@@ -742,6 +755,15 @@ export class SubagentContinuationManager {
742
755
  */
743
756
  async submitMaterialized(activation, content, source, parent, signal) {
744
757
  try {
758
+ if (contentHasImage(content)) {
759
+ // The capability read awaits with the activation already published, so
760
+ // the disposal cutoff is re-checked before the submit; a drain that
761
+ // began during the read turns into a clean closing rejection.
762
+ await this.assertImageCapable(activation.handle.agent, signal);
763
+ if (activation.disposal !== undefined) {
764
+ throw new SubagentError(`subagent "${activation.childId}" is closing`, 'ACTIVATION_CLOSING');
765
+ }
766
+ }
745
767
  return this.submitAdmitted(activation, content, source, parent, signal);
746
768
  }
747
769
  catch (error) {
@@ -751,6 +773,32 @@ export class SubagentContinuationManager {
751
773
  throw error;
752
774
  }
753
775
  }
776
+ /**
777
+ * Refuse image content addressed to a child whose model accepts text only.
778
+ * Callers guard with `contentHasImage`, so text-only delivery never awaits.
779
+ * The check runs inside the per-child delivery lock, before the message
780
+ * exists, so a rejection leaves no partial user message. When the child's
781
+ * route is not fixed by its options (a request-waterfall listener owns it)
782
+ * or no LLM registry is composed, delivery proceeds and the LLM layer's
783
+ * text-only projection replaces each image with its stable placeholder.
784
+ * @param agent - the live or freshly materialized child agent.
785
+ * @param signal - caller cancellation bounding the model-info read.
786
+ * @throws {SubagentError} `MODEL_DOES_NOT_SUPPORT_IMAGES` when the child's resolved model declines image input.
787
+ */
788
+ async assertImageCapable(agent, signal) {
789
+ const { provider, model } = agent.options;
790
+ if (provider === undefined || model === undefined)
791
+ return;
792
+ const llm = this.ctx.get('llm');
793
+ /* v8 ignore next -- a deployment without the LLM registry serves no model
794
+ * to refuse against; delivery then defers to the text-only projection. */
795
+ if (llm === undefined)
796
+ return;
797
+ const info = await llm.resolveModelInfo(provider, model, signal);
798
+ if (info.inputModalities !== undefined && !info.inputModalities.includes('image')) {
799
+ throw new SubagentError(`Model "${model}" does not support image input.`, 'MODEL_DOES_NOT_SUPPORT_IMAGES');
800
+ }
801
+ }
754
802
  /**
755
803
  * Create or resume the child Agent through the private activation-owner
756
804
  * scope, install the handle in a fresh Activation, and register ownership on
@@ -5,10 +5,9 @@
5
5
  *
6
6
  * @module @deepseek-ai/dsh-subagent/control-types
7
7
  */
8
- import type { EncodedImageAttachment } from '@deepseek-ai/dsh-attachment/types';
8
+ import type { PromptContentPart } from '@deepseek-ai/dsh-attachment/types';
9
9
  import type { Branded } from '@deepseek-ai/dsh-brand';
10
10
  import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
11
- import type { ContentBlock } from '@deepseek-ai/dsh-llm/types';
12
11
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
13
12
  /**
14
13
  * Client-minted identity of one browser prompt, persisted on the exact accepted
@@ -83,19 +82,6 @@ export type SubagentAddress = {
83
82
  } | {
84
83
  readonly mode: 'continuable';
85
84
  });
86
- /**
87
- * One browser-encoded upload as the Session prompt wire carries it: the shared
88
- * attachment vocabulary under the content-block tag.
89
- */
90
- export interface EncodedImagePromptBlock extends EncodedImageAttachment {
91
- readonly type: 'image';
92
- }
93
- /**
94
- * One block a browser prompt may carry. The encoded upload is accepted by the
95
- * wire and refused by the Host, so the Client narrows nothing: a caller that
96
- * attaches an image is answered, not silently stripped.
97
- */
98
- export type SubagentPromptContentPart = ContentBlock | EncodedImagePromptBlock;
99
85
  /** One human message addressed to a continuable direct child. */
100
86
  export interface SubagentPromptRequest {
101
87
  /** Identity persisted on the accepted message, minted before the call. */
@@ -104,8 +90,12 @@ export interface SubagentPromptRequest {
104
90
  readonly childSessionId: SessionId;
105
91
  /** Required discriminator retained from the browser control address. */
106
92
  readonly mode: 'continuable';
107
- /** Content proposed as the child's user message; images are refused. */
108
- readonly content: readonly SubagentPromptContentPart[];
93
+ /**
94
+ * Browser prompt parts delivered as the child's user message. The Host
95
+ * admits and persists image parts before delivery, so the wire never
96
+ * carries a durable attachment reference the caller could fabricate.
97
+ */
98
+ readonly content: readonly PromptContentPart[];
109
99
  /** Optional browser zone sampled for this exact human prompt. */
110
100
  readonly clientTimeZone?: string;
111
101
  }
@@ -118,10 +108,8 @@ export interface SubagentInterruptReceipt {
118
108
  readonly accepted: true;
119
109
  }
120
110
  /**
121
- * Failure details the control surface answers with. The catalog read, the
122
- * prompt, and the interrupt produce these codes; a Client fabricates
123
- * `subagent/not-resumable` and `subagent/delivery-unavailable` for a one-shot
124
- * address it refuses before the call, so both planes read one vocabulary.
111
+ * Failure details the control surface answers with. Catalog reads, prompts,
112
+ * and interrupts share this vocabulary with the Client Remote result.
125
113
  */
126
114
  declare module '@deepseek-ai/dsh-typert-protocol' {
127
115
  interface RemoteErrorDetailsMap {
@@ -141,12 +129,8 @@ declare module '@deepseek-ai/dsh-typert-protocol' {
141
129
  'subagent/unauthorized': {
142
130
  readonly childSessionId: SessionId;
143
131
  };
144
- /**
145
- * The continuation admits no attachment. `reason` names the refused plane
146
- * for the caller's copy, as the Session prompt's attachment refusals do.
147
- */
148
- 'subagent/attachment-unsupported': {
149
- readonly childSessionId: SessionId;
132
+ /** Image admission or model image-capability refusal. */
133
+ 'subagent/attachment-invalid': {
150
134
  readonly reason: string;
151
135
  };
152
136
  /** The child exists but its inbox cannot admit the message now. */
@@ -6,10 +6,9 @@
6
6
  * @module @deepseek-ai/dsh-subagent
7
7
  */
8
8
  import type { Context } from '@deepseek-ai/cordis';
9
- import type { ContentBlock } from '@deepseek-ai/dsh-llm';
10
9
  import type { SessionId } from '@deepseek-ai/dsh-session';
11
10
  import { z } from 'zod';
12
- import type { SubagentCatalog, SubagentListEntry, SubagentPromptContentPart } from './control-types.ts';
11
+ import type { SubagentCatalog, SubagentListEntry } from './control-types.ts';
13
12
  declare const CONTROL_ID_SCHEMAS: {
14
13
  readonly 'subagent.list': z.ZodObject<{
15
14
  parentSessionId: z.ZodString;
@@ -33,21 +32,6 @@ declare const CONTROL_ID_SCHEMAS: {
33
32
  * @throws {RemoteError} `gateway/bad-request` with the original Zod issues.
34
33
  */
35
34
  export declare function validateControlRequest(method: keyof typeof CONTROL_ID_SCHEMAS, payload: unknown): void;
36
- /**
37
- * Admit the content one continuation may deliver, refusing every image.
38
- *
39
- * The blocks become the child's user message verbatim, and this surface admits
40
- * no attachment: nothing here registers encoded bytes with the attachment
41
- * service, so an image would reach the child as a reference nothing resolves.
42
- * The wire accepts the encoded upload so this refusal — not a Client that
43
- * strips the block — is what the caller is answered with. Other block types
44
- * still cross unnarrowed.
45
- * @param childSessionId - the addressed child, named by the refusal.
46
- * @param content - blocks the caller asked to deliver.
47
- * @returns the admitted blocks, in order, as the durable content vocabulary.
48
- * @throws {RemoteError} `subagent/attachment-unsupported` when any block is an image.
49
- */
50
- export declare function admitPromptContent(childSessionId: SessionId, content: readonly SubagentPromptContentPart[]): ContentBlock[];
51
35
  /**
52
36
  * Project one durable listing onto the catalog view, replacing each row's
53
37
  * store-derived activity with the live Agent driver's status and reporting
@@ -5,6 +5,7 @@
5
5
  *
6
6
  * @module @deepseek-ai/dsh-subagent
7
7
  */
8
+ import { AttachmentError } from '@deepseek-ai/dsh-attachment';
8
9
  import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
9
10
  import { z } from 'zod';
10
11
  import { SubagentError } from "./error.js";
@@ -35,30 +36,6 @@ export function validateControlRequest(method, payload) {
35
36
  throw new RemoteError('gateway/bad-request', `invalid payload for ${method}`, { issues: parsed.error.issues });
36
37
  }
37
38
  }
38
- /**
39
- * Admit the content one continuation may deliver, refusing every image.
40
- *
41
- * The blocks become the child's user message verbatim, and this surface admits
42
- * no attachment: nothing here registers encoded bytes with the attachment
43
- * service, so an image would reach the child as a reference nothing resolves.
44
- * The wire accepts the encoded upload so this refusal — not a Client that
45
- * strips the block — is what the caller is answered with. Other block types
46
- * still cross unnarrowed.
47
- * @param childSessionId - the addressed child, named by the refusal.
48
- * @param content - blocks the caller asked to deliver.
49
- * @returns the admitted blocks, in order, as the durable content vocabulary.
50
- * @throws {RemoteError} `subagent/attachment-unsupported` when any block is an image.
51
- */
52
- export function admitPromptContent(childSessionId, content) {
53
- const admitted = [];
54
- for (const block of content) {
55
- if (block.type === 'image') {
56
- throw new RemoteError('subagent/attachment-unsupported', 'subagent continuation does not accept images', { childSessionId, reason: 'SUBAGENT_IMAGE_UNSUPPORTED' });
57
- }
58
- admitted.push(block);
59
- }
60
- return admitted;
61
- }
62
39
  /**
63
40
  * Project one durable listing onto the catalog view, replacing each row's
64
41
  * store-derived activity with the live Agent driver's status and reporting
@@ -109,8 +86,13 @@ export function rejectPrompt(error, childSessionId, signal) {
109
86
  if (isCancellation(error, signal)) {
110
87
  throw new RemoteError('gateway/cancelled', 'subagent prompt was cancelled', {}, { cause: error });
111
88
  }
89
+ if (error instanceof AttachmentError) {
90
+ throw new RemoteError('subagent/attachment-invalid', error.message, { reason: error.code }, { cause: error });
91
+ }
112
92
  if (error instanceof SubagentError) {
113
93
  switch (error.code) {
94
+ case 'MODEL_DOES_NOT_SUPPORT_IMAGES':
95
+ throw new RemoteError('subagent/attachment-invalid', error.message, { reason: error.code }, { cause: error });
114
96
  case 'NOT_RESUMABLE':
115
97
  throw new RemoteError('subagent/not-resumable', 'subagent cannot be resumed', { childSessionId }, { cause: error });
116
98
  case 'UNAUTHORIZED':
@@ -248,10 +248,12 @@ export declare class SubagentRuntime extends TypertRemoteService {
248
248
  * validated browser zone on the accepted message. Success identifies the
249
249
  * message the child's FIFO inbox accepted; later execution is independent of
250
250
  * this call.
251
+ * Image parts are admitted and persisted through the attachment store
252
+ * before delivery, and the child's model must accept image input.
251
253
  * @param request - durable address, minted identity, content, and optional browser zone.
252
254
  * @param signal - carrier cancellation, owning the call until inbox acceptance.
253
255
  * @returns the accepted message's inbox identity.
254
- * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-unsupported`,
256
+ * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-invalid`,
255
257
  * `subagent/invalid-time-zone`, `subagent/parent-unavailable`,
256
258
  * `subagent/not-resumable`, `subagent/unauthorized`,
257
259
  * `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.
@@ -62,11 +62,12 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
62
62
  if (target) Object.defineProperty(target, contextIn.name, descriptor);
63
63
  done = true;
64
64
  };
65
+ import { admitPromptContent } from '@deepseek-ai/dsh-attachment';
65
66
  import { scopeTarget } from '@deepseek-ai/dsh-scope';
66
67
  import { assertObjectJsonSchema } from '@deepseek-ai/dsh-tools';
67
68
  import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time';
68
69
  import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
69
- import { admitPromptContent, catalogView, rejectCatalogRead, rejectPrompt, validateControlRequest, } from "./control.js";
70
+ import { catalogView, rejectCatalogRead, rejectPrompt, validateControlRequest, } from "./control.js";
70
71
  import { SubagentError } from "./error.js";
71
72
  import { assertSubagentMaxDepth } from "./depth.js";
72
73
  import { createActivationObserver, createLifecycleEmitter, observeRun } from "./lifecycle.js";
@@ -305,10 +306,12 @@ let SubagentRuntime = (() => {
305
306
  * validated browser zone on the accepted message. Success identifies the
306
307
  * message the child's FIFO inbox accepted; later execution is independent of
307
308
  * this call.
309
+ * Image parts are admitted and persisted through the attachment store
310
+ * before delivery, and the child's model must accept image input.
308
311
  * @param request - durable address, minted identity, content, and optional browser zone.
309
312
  * @param signal - carrier cancellation, owning the call until inbox acceptance.
310
313
  * @returns the accepted message's inbox identity.
311
- * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-unsupported`,
314
+ * @throws {RemoteError} `gateway/bad-request`, `subagent/attachment-invalid`,
312
315
  * `subagent/invalid-time-zone`, `subagent/parent-unavailable`,
313
316
  * `subagent/not-resumable`, `subagent/unauthorized`,
314
317
  * `subagent/delivery-unavailable`, `gateway/cancelled`, or `gateway/internal`.
@@ -316,7 +319,6 @@ let SubagentRuntime = (() => {
316
319
  async prompt(request, signal) {
317
320
  const { parentSessionId, childSessionId, clientTimeZone } = request;
318
321
  validateControlRequest('subagent.prompt', request);
319
- const content = admitPromptContent(childSessionId, request.content);
320
322
  const canonicalTimeZone = clientTimeZone === undefined
321
323
  ? undefined
322
324
  : canonicalClientTimeZone(clientTimeZone);
@@ -333,6 +335,18 @@ let SubagentRuntime = (() => {
333
335
  ...(canonicalTimeZone === undefined ? {} : { clientTimeZone: canonicalTimeZone }),
334
336
  };
335
337
  try {
338
+ // Admission precedes delivery: image parts become durable references
339
+ // here, so the child inbox only ever accepts Host-persisted attachments.
340
+ let content;
341
+ if (request.content.every((part) => part.type === 'text')) {
342
+ content = request.content.map(part => ({ type: 'text', text: part.text }));
343
+ }
344
+ else {
345
+ const attachments = this.ctx.get('attachments');
346
+ if (attachments === undefined)
347
+ throw new Error('subagent image prompt requires an attachment store');
348
+ content = await admitPromptContent(attachments, request.content);
349
+ }
336
350
  return { messageId: await this.followup(parent, childSessionId, content, { source, signal }) };
337
351
  }
338
352
  catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-subagent",
3
3
  "description": "Abstract subagent seam (ctx.subagents): named-provider registry for delegating to child agents",
4
- "version": "0.1.2-alpha.2",
4
+ "version": "0.1.2-alpha.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -50,30 +50,30 @@
50
50
  "license": "MIT",
51
51
  "dependencies": {
52
52
  "zod": "^4.4.3",
53
- "@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
54
- "@deepseek-ai/dsh-util-values": "^0.1.2-alpha.2"
53
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.3",
54
+ "@deepseek-ai/dsh-util-values": "^0.1.2-alpha.3"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@deepseek-ai/cordis": "^4.0.2",
58
- "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
59
- "@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.2",
60
- "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.2",
61
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
62
- "@deepseek-ai/dsh-jobs": "^0.1.2-alpha.2",
63
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
64
- "@deepseek-ai/dsh-sandbox": "^0.1.2-alpha.2",
65
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.2-alpha.2",
66
- "@deepseek-ai/dsh-scope": "^0.1.2-alpha.2",
67
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
68
- "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.2",
69
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.2-alpha.2",
70
- "@deepseek-ai/dsh-session-query": "^0.1.2-alpha.2",
71
- "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
72
- "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
73
- "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.2",
74
- "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
75
- "@deepseek-ai/dsh-user-approval": "^0.1.2-alpha.2",
76
- "@deepseek-ai/dsh-util-time": "^0.1.2-alpha.2"
58
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.3",
59
+ "@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.3",
60
+ "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.3",
61
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
62
+ "@deepseek-ai/dsh-jobs": "^0.1.2-alpha.3",
63
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3",
64
+ "@deepseek-ai/dsh-sandbox": "^0.1.2-alpha.3",
65
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.2-alpha.3",
66
+ "@deepseek-ai/dsh-scope": "^0.1.2-alpha.3",
67
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
68
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.3",
69
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.2-alpha.3",
70
+ "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.3",
71
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.3",
72
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.3",
73
+ "@deepseek-ai/dsh-user-approval": "^0.1.2-alpha.3",
74
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.3",
75
+ "@deepseek-ai/dsh-util-time": "^0.1.2-alpha.3",
76
+ "@deepseek-ai/dsh-session-query": "^0.1.2-alpha.3"
77
77
  },
78
78
  "peerDependenciesMeta": {
79
79
  "@deepseek-ai/dsh-agent-presets": {
@@ -106,27 +106,27 @@
106
106
  },
107
107
  "devDependencies": {
108
108
  "@deepseek-ai/cordis": "^4.0.2",
109
- "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
110
- "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.2",
111
- "@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.2",
112
- "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
113
- "@deepseek-ai/dsh-jobs": "^0.1.2-alpha.2",
114
- "@deepseek-ai/dsh-llm": "^0.1.2-alpha.2",
115
- "@deepseek-ai/dsh-sandbox": "^0.1.2-alpha.2",
116
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.2-alpha.2",
117
- "@deepseek-ai/dsh-scope": "^0.1.2-alpha.2",
118
- "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
119
- "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.2",
120
- "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.2",
121
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.2-alpha.2",
122
- "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.2",
123
- "@deepseek-ai/dsh-storage-json": "^0.1.2-alpha.2",
124
- "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
125
- "@deepseek-ai/dsh-tools": "^0.1.2-alpha.2",
126
- "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.2",
127
- "@deepseek-ai/dsh-user-approval": "^0.1.2-alpha.2",
128
- "@deepseek-ai/dsh-util-time": "^0.1.2-alpha.2",
129
- "@deepseek-ai/dsh-storage": "^0.1.2-alpha.2",
130
- "@deepseek-ai/dsh-session-query": "^0.1.2-alpha.2"
109
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.3",
110
+ "@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.3",
111
+ "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.3",
112
+ "@deepseek-ai/dsh-jobs": "^0.1.2-alpha.3",
113
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.3",
114
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.3",
115
+ "@deepseek-ai/dsh-sandbox": "^0.1.2-alpha.3",
116
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.2-alpha.3",
117
+ "@deepseek-ai/dsh-scope": "^0.1.2-alpha.3",
118
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.3",
119
+ "@deepseek-ai/dsh-session-projection": "^0.1.2-alpha.3",
120
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.2-alpha.3",
121
+ "@deepseek-ai/dsh-session-query": "^0.1.2-alpha.3",
122
+ "@deepseek-ai/dsh-storage": "^0.1.2-alpha.3",
123
+ "@deepseek-ai/dsh-storage-domain": "^0.1.2-alpha.3",
124
+ "@deepseek-ai/dsh-storage-json": "^0.1.2-alpha.3",
125
+ "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.3",
126
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.3",
127
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.2-alpha.3",
128
+ "@deepseek-ai/dsh-user-approval": "^0.1.2-alpha.3",
129
+ "@deepseek-ai/dsh-util-time": "^0.1.2-alpha.3",
130
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.3"
131
131
  }
132
132
  }