@yanlinglabs/winter-agent-sdk 0.0.38 → 0.0.40

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.md CHANGED
@@ -176,6 +176,34 @@ All three report the icon of each site they name to the HOST only, as `winter_si
176
176
 
177
177
  Subagents inherit both.
178
178
 
179
+ ### Messaging a host's other sessions (0.0.39)
180
+
181
+ **`Options.hostMessaging`** — `{ send, list }`, for a host that runs many sessions (one process or one
182
+ Worker each) and wants `SendMessage` and `ListAgents` to reach the others. With it set, `query()` puts
183
+ `hostMessaging: true` on the wire and answers two runtime → host control requests,
184
+ `host_message_send` and `host_message_list` (`HOST_MESSAGE_SEND_SUBTYPE` / `HOST_MESSAGE_LIST_SUBTYPE`),
185
+ the same way for a spawned `winter` process and an embedded Worker:
186
+
187
+ - `SendMessage` resolves `to` in-process first — the session's own subagents, then the in-process
188
+ peers. Only a `not_found` there goes to `send`, with the model's raw `to`, the message, its summary,
189
+ `notifyWhenIdle`, the runtime's message id and (for a subagent's call) `fromAgentId`. The host's
190
+ answer (`delivered`, `queued`, `resumed_and_delivered`, `refused`, `not_found`, `unavailable`,
191
+ `delivery_uncertain`, plus an optional `notify` fact and a one-sentence `note`) is the tool's
192
+ result under the runtime's message id, so a retry of the same tool call is answered from the
193
+ ledger and never asks the host twice. A throwing or malformed `send` is `delivery_uncertain`.
194
+ - `ListAgents` lists the subagents, then the sessions `list` returns as `session` rows, ending with
195
+ a count of the ones the host left out (`omitted`) — a listing is never silently cut. A failing
196
+ `list` lists nothing more.
197
+ - `TaskStop` with a `task_id` that names no task of this session goes to the optional `stop`
198
+ (`host_session_stop`): the host interrupts that session's running turn and answers `stopped`,
199
+ `not_running`, `refused`, `not_found` or `unavailable`.
200
+ - The calling tool's abort signal travels with the request: an interrupted call cancels it
201
+ (`control_cancel_request`), and the handler's own `signal` aborts so the host can skip the delivery.
202
+
203
+ The handler is per session and never told who is sending: it knows its caller by construction. The
204
+ router core reaches the same seam through `MessagingRuntimeDeps.hostMessaging` (a `HostMessagingPort`
205
+ per owning session, on the `/messaging` subpath). Without the option, nothing changes.
206
+
179
207
  ### The 0.0.16 background-default change
180
208
 
181
209
  Before 0.0.16, an Agent tool call with no `run_in_background` ran in the **foreground** (this call
@@ -419,6 +419,138 @@ function facetNotificationKey2(sessionId) {
419
419
  function isReservedNotificationKey2(key) {
420
420
  return key.startsWith(RESERVED_NOTIFICATION_KEY_PREFIX2);
421
421
  }
422
+ // src/messaging/host.ts
423
+ var SEND_STATUSES = new Set(["delivered", "queued", "resumed_and_delivered", "refused", "not_found", "unavailable", "delivery_uncertain"]);
424
+ var NEEDS_REASON = new Set(["refused", "not_found", "unavailable", "delivery_uncertain"]);
425
+ var LISTED_STATUSES = new Set(["starting", "running", "idle", "exited", "unavailable", "archived"]);
426
+ var HOST_MESSAGE_NOTE_MAX2 = 500;
427
+ var HOST_MESSAGE_LIST_MAX2 = 200;
428
+ function isRecord(v) {
429
+ return typeof v === "object" && v !== null && !Array.isArray(v);
430
+ }
431
+ function isHostMessageSendRequest2(payload) {
432
+ if (!isRecord(payload))
433
+ return false;
434
+ return typeof payload.to === "string" && typeof payload.message === "string" && typeof payload.messageId === "string" && (payload.summary === undefined || typeof payload.summary === "string") && (payload.notifyWhenIdle === undefined || typeof payload.notifyWhenIdle === "boolean") && (payload.fromAgentId === undefined || typeof payload.fromAgentId === "string");
435
+ }
436
+ function isHostMessageListRequest2(payload) {
437
+ if (payload === undefined)
438
+ return true;
439
+ return isRecord(payload) && (payload.fromAgentId === undefined || typeof payload.fromAgentId === "string");
440
+ }
441
+ function isHostSessionStopRequest2(payload) {
442
+ return isRecord(payload) && typeof payload.id === "string" && (payload.fromAgentId === undefined || typeof payload.fromAgentId === "string");
443
+ }
444
+ var STOP_STATUSES = new Set(["stopped", "not_running", "refused", "not_found", "unavailable"]);
445
+ var STOP_NEEDS_REASON = new Set(["refused", "not_found", "unavailable"]);
446
+ function isHostSessionStopAnswer2(value) {
447
+ if (!isRecord(value))
448
+ return false;
449
+ if (typeof value.status !== "string" || !STOP_STATUSES.has(value.status))
450
+ return false;
451
+ if (value.reason !== undefined && typeof value.reason !== "string")
452
+ return false;
453
+ if (STOP_NEEDS_REASON.has(value.status) && (typeof value.reason !== "string" || value.reason.length === 0))
454
+ return false;
455
+ if (value.note !== undefined && typeof value.note !== "string")
456
+ return false;
457
+ return true;
458
+ }
459
+ function isHostMessageSendAnswer2(value) {
460
+ if (!isRecord(value))
461
+ return false;
462
+ if (typeof value.status !== "string" || !SEND_STATUSES.has(value.status))
463
+ return false;
464
+ if (value.reason !== undefined && typeof value.reason !== "string")
465
+ return false;
466
+ if (NEEDS_REASON.has(value.status) && (typeof value.reason !== "string" || value.reason.length === 0))
467
+ return false;
468
+ if (value.retryable !== undefined && typeof value.retryable !== "boolean")
469
+ return false;
470
+ if (value.note !== undefined && typeof value.note !== "string")
471
+ return false;
472
+ if (value.notify !== undefined) {
473
+ if (!isRecord(value.notify))
474
+ return false;
475
+ if (value.notify.subscribed !== undefined && value.notify.subscribed !== true)
476
+ return false;
477
+ if (value.notify.refused !== undefined && typeof value.notify.refused !== "string")
478
+ return false;
479
+ }
480
+ return true;
481
+ }
482
+ function isHostReachableSession(value) {
483
+ if (!isRecord(value))
484
+ return false;
485
+ if (typeof value.address !== "string" || !validateToField2(value.address).ok)
486
+ return false;
487
+ if (value.name !== undefined && (typeof value.name !== "string" || value.name.includes(`
488
+ `)))
489
+ return false;
490
+ if (typeof value.status !== "string" || !LISTED_STATUSES.has(value.status))
491
+ return false;
492
+ if (typeof value.mode !== "string")
493
+ return false;
494
+ if (value.cwd !== undefined && typeof value.cwd !== "string")
495
+ return false;
496
+ return true;
497
+ }
498
+ function normaliseHostMessageListAnswer2(value) {
499
+ if (!isRecord(value) || !Array.isArray(value.sessions))
500
+ return;
501
+ const valid = value.sessions.filter(isHostReachableSession);
502
+ const sessions = valid.slice(0, HOST_MESSAGE_LIST_MAX2);
503
+ const hostOmitted = typeof value.omitted === "number" && Number.isInteger(value.omitted) && value.omitted > 0 ? value.omitted : 0;
504
+ const omitted = hostOmitted + (valid.length - sessions.length);
505
+ return {
506
+ ...omitted > 0 ? { omitted } : {},
507
+ sessions: sessions.map((s) => ({
508
+ address: s.address,
509
+ ...s.name !== undefined ? { name: s.name } : {},
510
+ status: s.status,
511
+ mode: s.mode,
512
+ ...s.cwd !== undefined ? { cwd: s.cwd } : {}
513
+ }))
514
+ };
515
+ }
516
+ function hostSessionToListed2(row) {
517
+ const reachable = row.status !== "archived" && row.status !== "unavailable";
518
+ return {
519
+ address: row.address,
520
+ ...row.name !== undefined ? { name: row.name } : {},
521
+ objectKind: "session",
522
+ runtimeKind: "winter-agent",
523
+ status: row.status,
524
+ mode: row.mode,
525
+ ...row.cwd !== undefined ? { cwd: row.cwd } : {},
526
+ capabilities: { message: reachable, resume: reachable, notifyWhenIdle: false, reply: reachable }
527
+ };
528
+ }
529
+ function hostAnswerToOutcome2(messageId, answer) {
530
+ const reason = answer.reason ?? answer.status;
531
+ switch (answer.status) {
532
+ case "delivered":
533
+ case "queued":
534
+ case "resumed_and_delivered":
535
+ return { status: answer.status, messageId };
536
+ case "refused":
537
+ return { status: "refused", messageId, reason };
538
+ case "not_found":
539
+ return { status: "not_found", messageId, reason };
540
+ case "unavailable":
541
+ return { status: "unavailable", messageId, retryable: answer.retryable === true, reason };
542
+ case "delivery_uncertain":
543
+ return { status: "delivery_uncertain", messageId, deliveryMayHaveOccurred: true, reason };
544
+ }
545
+ }
546
+ function boundedHostNote2(note) {
547
+ if (note === undefined)
548
+ return;
549
+ const trimmed = note.trim();
550
+ if (trimmed.length === 0)
551
+ return;
552
+ return trimmed.length > HOST_MESSAGE_NOTE_MAX2 ? `${trimmed.slice(0, HOST_MESSAGE_NOTE_MAX2 - 1)}…` : trimmed;
553
+ }
422
554
  // src/messaging/router.ts
423
555
  var MAX_TRACKED_MESSAGE_IDS2 = 1e4;
424
556
  function rememberBounded2(map, key, value, cap = MAX_TRACKED_MESSAGE_IDS2) {
@@ -484,15 +616,15 @@ var SUCCESS_CLASS_STATUSES = new Set(["delivered", "queued", "resumed_and_delive
484
616
  function outcomeReason(outcome) {
485
617
  return "reason" in outcome ? outcome.reason : outcome.status;
486
618
  }
487
- async function sendMessage2(deps, caller, input) {
619
+ async function sendMessage2(deps, caller, input, opts = {}) {
488
620
  const now = deps.now();
489
621
  const messageId = deps.seam.allocateMessageId(caller.sessionId, caller.toolUseId);
490
622
  const existing = deps.seam.lookupOutcome(messageId);
491
623
  if (existing !== undefined)
492
624
  return { outcome: existing };
493
- function settle(outcome, notify) {
625
+ function settle(outcome, notify, note) {
494
626
  deps.seam.recordOutcome(messageId, outcome);
495
- return notify !== undefined ? { outcome, notify } : { outcome };
627
+ return { outcome, ...notify !== undefined ? { notify } : {}, ...note !== undefined ? { note } : {} };
496
628
  }
497
629
  const from = callerAddress2(caller);
498
630
  const fromKey = serializeRuntimeAddress2(from);
@@ -512,8 +644,12 @@ async function sendMessage2(deps, caller, input) {
512
644
  const reachable = await deps.adapter.listReachable({ parent: buildSessionAddress2(caller.sessionId) });
513
645
  const peers = reachable.filter((r) => r.objectKind === "session");
514
646
  const resolved = resolveTarget2({ to: input.to, callerParentSessionId: caller.sessionId, children: deps.seam.children(), peers });
515
- if (resolved.kind === "not_found")
516
- return settle(notFound2(messageId, resolved.message));
647
+ if (resolved.kind === "not_found") {
648
+ const host = deps.hostMessaging?.(caller.sessionId);
649
+ if (host === undefined)
650
+ return settle(notFound2(messageId, resolved.message));
651
+ return settle(...await askHost(host, messageId, caller, input, opts.signal));
652
+ }
517
653
  if (resolved.kind === "stale")
518
654
  return settle(refused2(messageId, resolved.message));
519
655
  if (resolved.kind === "ambiguous")
@@ -560,6 +696,25 @@ async function sendMessage2(deps, caller, input) {
560
696
  const idleOutcome = await deps.adapter.subscribeIdle(to, { messageId });
561
697
  return settle(bodyOutcome, idleOutcome.status === "subscribed" ? { subscribed: true } : { refused: outcomeReason(idleOutcome) });
562
698
  }
699
+ async function askHost(host, messageId, caller, input, signal) {
700
+ let answer;
701
+ try {
702
+ answer = await host.send({
703
+ to: input.to,
704
+ message: input.message,
705
+ ...input.summary !== undefined ? { summary: input.summary } : {},
706
+ ...input.notify_when_idle !== undefined ? { notifyWhenIdle: input.notify_when_idle } : {},
707
+ messageId,
708
+ ...caller.agentId !== undefined ? { fromAgentId: caller.agentId } : {}
709
+ }, signal !== undefined ? { signal } : {});
710
+ } catch (err) {
711
+ return [deliveryUncertain2(messageId, `the host did not answer the delivery: ${describeThrow(err)}`)];
712
+ }
713
+ if (!isHostMessageSendAnswer2(answer))
714
+ return [deliveryUncertain2(messageId, "the host answered the delivery with a malformed outcome")];
715
+ const notify = answer.notify === undefined ? undefined : { ...answer.notify.subscribed === true ? { subscribed: true } : {}, ...answer.notify.refused !== undefined ? { refused: answer.notify.refused } : {} };
716
+ return [hostAnswerToOutcome2(messageId, answer), notify, boundedHostNote2(answer.note)];
717
+ }
563
718
  function describeThrow(err) {
564
719
  return err instanceof Error ? err.message : String(err);
565
720
  }
@@ -593,12 +748,32 @@ function formatListing(rows) {
593
748
  }).join(`
594
749
  `);
595
750
  }
596
- async function listAgents2(deps, caller, _input) {
751
+ function omittedLine(omitted) {
752
+ return `(${omitted} more reachable session${omitted === 1 ? "" : "s"} not listed)`;
753
+ }
754
+ async function listAgents2(deps, caller, _input, opts = {}) {
597
755
  const selfAddr = buildSessionAddress2(caller.sessionId);
598
756
  const selfKey = serializeRuntimeAddress2(selfAddr);
599
757
  const reachable = await deps.adapter.listReachable({ parent: selfAddr });
600
758
  const rows = reachable.filter((r) => r.address !== selfKey);
601
- return { listing: formatListing(rows), rows };
759
+ const host = deps.hostMessaging?.(caller.sessionId);
760
+ let omitted = 0;
761
+ if (host !== undefined) {
762
+ const answer = normaliseHostMessageListAnswer2(await host.list({}, opts.signal !== undefined ? { signal: opts.signal } : {}).catch(() => {
763
+ return;
764
+ }));
765
+ omitted = answer?.omitted ?? 0;
766
+ const seen = new Set(rows.map((r) => r.address));
767
+ for (const row of answer?.sessions ?? []) {
768
+ if (seen.has(row.address) || row.address === selfKey)
769
+ continue;
770
+ seen.add(row.address);
771
+ rows.push(hostSessionToListed2(row));
772
+ }
773
+ }
774
+ const listing = omitted > 0 ? `${formatListing(rows)}
775
+ ${omittedLine(omitted)}` : formatListing(rows);
776
+ return { listing, rows, ...omitted > 0 ? { omitted } : {} };
602
777
  }
603
778
  function readNotifications2(deps, caller) {
604
779
  return deps.notifications.drain(caller.sessionId);
@@ -611,4 +786,4 @@ function createMessagingRouter2(deps) {
611
786
  readNotifications: (caller) => readNotifications2(deps, caller)
612
787
  };
613
788
  }
614
- export { serializeRuntimeAddress2, createFakeMessagingRouterSeam2, REFERENCE_RUNTIME_KIND2, validateToField2, buildSessionAddress2, buildChildAddress2, parseRuntimeAddress2, sameAddress2, MAX_GLOBAL_MESSAGE_SIZE2, DEFAULT_MESSAGE_TTL_MS2, MAX_HOP_COUNT2, RAPID_REPEAT_WINDOW_MS2, HELD_INBOX_CAP2, ACCEPTED_QUEUE_CAP2, DEFAULT_HOLD_EXPIRY_MS2, NOTIFY_IDLE_EXPIRY_MS2, messageExceedsMaxSize2, hopCountExceeded2, delivered2, queued2, resumedAndDelivered2, held2, subscribed2, deliveryUncertain2, refused2, ambiguous2, notFound2, unavailable2, createLoopGuard2, childToListedRuntimeObject2, resolveTarget2, classifyPermissionMode2, mapFromModeToPermissionClass2, defaultInboundResult2, resolveInboundDecision2, createMailbox2, buildDefaultHoldEntry2, buildExplicitHoldEntry2, isIdleSubscribeSenderAllowed2, isIdleSubscribeTargetAllowed2, createNotificationQueue2, createIdleSubscriptionStore2, AGENT_MESSAGE_TAG2, escapeAttributionText2, escapeAttributionAttribute2, RESERVED_NOTIFICATION_KEY_PREFIX2, facetNotificationKey2, isReservedNotificationKey2, MAX_TRACKED_MESSAGE_IDS2, rememberBounded2, createMessagingRouterSeam2, createSubscriberDirectory2, callerAddress2, sendMessage2, formatListing, listAgents2, readNotifications2, createMessagingRouter2 };
789
+ export { serializeRuntimeAddress2, createFakeMessagingRouterSeam2, REFERENCE_RUNTIME_KIND2, validateToField2, buildSessionAddress2, buildChildAddress2, parseRuntimeAddress2, sameAddress2, HOST_MESSAGE_NOTE_MAX2, HOST_MESSAGE_LIST_MAX2, isHostMessageSendRequest2, isHostMessageListRequest2, isHostSessionStopRequest2, isHostSessionStopAnswer2, isHostMessageSendAnswer2, normaliseHostMessageListAnswer2, hostSessionToListed2, hostAnswerToOutcome2, boundedHostNote2, MAX_GLOBAL_MESSAGE_SIZE2, DEFAULT_MESSAGE_TTL_MS2, MAX_HOP_COUNT2, RAPID_REPEAT_WINDOW_MS2, HELD_INBOX_CAP2, ACCEPTED_QUEUE_CAP2, DEFAULT_HOLD_EXPIRY_MS2, NOTIFY_IDLE_EXPIRY_MS2, messageExceedsMaxSize2, hopCountExceeded2, delivered2, queued2, resumedAndDelivered2, held2, subscribed2, deliveryUncertain2, refused2, ambiguous2, notFound2, unavailable2, createLoopGuard2, childToListedRuntimeObject2, resolveTarget2, classifyPermissionMode2, mapFromModeToPermissionClass2, defaultInboundResult2, resolveInboundDecision2, createMailbox2, buildDefaultHoldEntry2, buildExplicitHoldEntry2, isIdleSubscribeSenderAllowed2, isIdleSubscribeTargetAllowed2, createNotificationQueue2, createIdleSubscriptionStore2, AGENT_MESSAGE_TAG2, escapeAttributionText2, escapeAttributionAttribute2, RESERVED_NOTIFICATION_KEY_PREFIX2, facetNotificationKey2, isReservedNotificationKey2, MAX_TRACKED_MESSAGE_IDS2, rememberBounded2, createMessagingRouterSeam2, createSubscriberDirectory2, callerAddress2, sendMessage2, formatListing, omittedLine, listAgents2, readNotifications2, createMessagingRouter2 };
package/dist/index.d.ts CHANGED
@@ -3,7 +3,8 @@ export type { Query, SdkMessage, SessionMessagingFacet } from "./query.js";
3
3
  export type { QueryInternal, ControlRequestHandler, ControlRequestHandlerResult } from "./query.js";
4
4
  export type { Options } from "./options.js";
5
5
  export { SYSTEM_PROMPT_DYNAMIC_BOUNDARY, DEFAULT_CONTEXT_WINDOW_TOKENS, DEFAULT_COMPACTION_THRESHOLD, DEFAULT_PLANS_DIRECTORY, DEFAULT_OUTPUT_STYLE } from "./options.js";
6
- export { DEFAULT_PROVIDER_STALL_TIMEOUT_MS, DEFAULT_KEYCHAIN_SERVICE, TEST_KEYCHAIN_ENV, TEST_KEYCHAIN_MEMORY, MCP_OAUTH_REFRESH_SUBTYPE, CREDENTIAL_RESOLVE_SUBTYPE } from "./options.js";
6
+ export { DEFAULT_PROVIDER_STALL_TIMEOUT_MS, DEFAULT_KEYCHAIN_SERVICE, TEST_KEYCHAIN_ENV, TEST_KEYCHAIN_MEMORY, MCP_OAUTH_REFRESH_SUBTYPE, CREDENTIAL_RESOLVE_SUBTYPE, HOST_MESSAGE_SEND_SUBTYPE, HOST_MESSAGE_LIST_SUBTYPE, HOST_SESSION_STOP_SUBTYPE } from "./options.js";
7
+ export type { HostMessagingHandler } from "./options.js";
7
8
  export { WEB_TOOLS_DEFAULTS, resolveWebToolsConfig } from "./options.js";
8
9
  export type { ResolvedWebToolsConfig } from "./options.js";
9
10
  export type { WebToolsConfig, WebSearchConfig, WebFetchConfig, WebPrivateAddressPolicy, AutoMemoryConfig } from "./protocol/config.js";
@@ -22,7 +23,7 @@ export type { SpawnedRuntimeProcess, SpawnRuntimeOptions, SpawnClaudeCodeProcess
22
23
  export type { RuntimeConfig, RuntimeHooksConfig, RuntimeHookMatcherGroup, SandboxSettingsConfig } from "./protocol/config.js";
23
24
  export type { McpServerConfigForProcessTransport, McpVersionNegotiation, AgentMcpServerSpec, RuntimeAgentDefinition } from "./protocol/config.js";
24
25
  export type { McpOAuthConfig, McpOAuthSecretRef, McpOAuthRefreshRequest, McpOAuthRefreshAnswer } from "./protocol/config.js";
25
- export type { CredentialResolveRequest, CredentialResolveAnswer } from "./protocol/config.js";
26
+ export type { CredentialResolveRequest, CredentialResolveAnswer, HostMessageSendRequest, HostMessageSendAnswer, HostMessageListRequest, HostMessageListAnswer, HostReachableSession, HostSessionStopRequest, HostSessionStopAnswer } from "./protocol/config.js";
26
27
  export { encodeFrame, decodeFrame, splitFrames, ProtocolError } from "./protocol/codec.js";
27
28
  export { PROTOCOL_VERSION } from "./protocol/frames.js";
28
29
  export { SDK_VERSION } from "./version.js";
package/dist/index.js CHANGED
@@ -16,10 +16,16 @@ import {
16
16
  WinterCompatibilitySessionStore2
17
17
  } from "./index-0xks1t4r.js";
18
18
  import {
19
+ isHostMessageSendRequest2,
20
+ isHostMessageListRequest2,
21
+ isHostSessionStopRequest2,
22
+ isHostSessionStopAnswer2,
23
+ isHostMessageSendAnswer2,
24
+ normaliseHostMessageListAnswer2,
19
25
  deliveryUncertain2,
20
26
  refused2,
21
27
  unavailable2
22
- } from "./index-51ysrfm8.js";
28
+ } from "./index-3zhkc4nb.js";
23
29
 
24
30
  // src/query.ts
25
31
  import { randomUUID } from "node:crypto";
@@ -70,6 +76,9 @@ var TEST_KEYCHAIN_ENV = "WINTER_TEST_KEYCHAIN";
70
76
  var TEST_KEYCHAIN_MEMORY = "memory";
71
77
  var MCP_OAUTH_REFRESH_SUBTYPE = "mcp_oauth_refresh";
72
78
  var CREDENTIAL_RESOLVE_SUBTYPE = "credential_resolve";
79
+ var HOST_MESSAGE_SEND_SUBTYPE = "host_message_send";
80
+ var HOST_MESSAGE_LIST_SUBTYPE = "host_message_list";
81
+ var HOST_SESSION_STOP_SUBTYPE = "host_session_stop";
73
82
  var WEB_TOOLS_DEFAULTS = {
74
83
  searchEnabled: true,
75
84
  maxSearchesPerCall: 8,
@@ -420,13 +429,15 @@ function toWireMcpServers(servers) {
420
429
  name: cfg.name,
421
430
  ...cfg.timeout !== undefined ? { timeout: cfg.timeout } : {},
422
431
  ...isWinterMcpServerInstance(cfg.instance) ? { tools: cfg.instance.listTools() } : {},
423
- ...cfg.toolNames !== undefined ? { toolNames: { ...cfg.toolNames } } : {}
432
+ ...cfg.toolNames !== undefined ? { toolNames: { ...cfg.toolNames } } : {},
433
+ ...cfg.toolLanes !== undefined ? { toolLanes: { ...cfg.toolLanes } } : {}
424
434
  } : cfg;
425
435
  }
426
436
  return out;
427
437
  }
438
+ var ANSWERED_AFTER_CANCEL = new Set(["sdk_mcp_call"]);
428
439
  function makeSdkMcpCallHandler(mcpServers) {
429
- return async (payload) => {
440
+ return async (payload, handlerCtx) => {
430
441
  const req = payload;
431
442
  const cfg = req.server !== undefined ? mcpServers[req.server] : undefined;
432
443
  if (!cfg || cfg.type !== "sdk") {
@@ -442,7 +453,7 @@ function makeSdkMcpCallHandler(mcpServers) {
442
453
  };
443
454
  }
444
455
  try {
445
- const result = await cfg.instance.callTool(req.tool ?? "", req.arguments ?? {});
456
+ const result = await cfg.instance.callTool(req.tool ?? "", req.arguments ?? {}, ...handlerCtx !== undefined ? [{ signal: handlerCtx.signal }] : []);
446
457
  return { ok: true, payload: result };
447
458
  } catch (err) {
448
459
  const message = err instanceof Error ? err.message : String(err);
@@ -551,6 +562,79 @@ function makeCredentialResolveHandler(onCredentialResolve, abortController) {
551
562
  }
552
563
  };
553
564
  }
565
+ function abortableFor(abortController, handlerCtx) {
566
+ const controller = new AbortController;
567
+ const onAbort = () => controller.abort();
568
+ if (abortController?.signal.aborted === true || handlerCtx?.signal.aborted === true)
569
+ controller.abort();
570
+ abortController?.signal.addEventListener("abort", onAbort, { once: true });
571
+ handlerCtx?.signal.addEventListener("abort", onAbort, { once: true });
572
+ return {
573
+ controller,
574
+ dispose: () => {
575
+ abortController?.signal.removeEventListener("abort", onAbort);
576
+ handlerCtx?.signal.removeEventListener("abort", onAbort);
577
+ }
578
+ };
579
+ }
580
+ function makeHostMessageSendHandler(host, abortController) {
581
+ return async (payload, handlerCtx) => {
582
+ if (!isHostMessageSendRequest2(payload))
583
+ return { ok: false, error: { code: "invalid_payload", message: "host_message_send expects { to, message, messageId, summary?, notifyWhenIdle?, fromAgentId? }" } };
584
+ const { controller, dispose } = abortableFor(abortController, handlerCtx);
585
+ const request = {
586
+ to: payload.to,
587
+ message: payload.message,
588
+ messageId: payload.messageId,
589
+ ...payload.summary !== undefined ? { summary: payload.summary } : {},
590
+ ...payload.notifyWhenIdle !== undefined ? { notifyWhenIdle: payload.notifyWhenIdle } : {},
591
+ ...payload.fromAgentId !== undefined ? { fromAgentId: payload.fromAgentId } : {}
592
+ };
593
+ try {
594
+ const answer = await host.send(request, { signal: controller.signal });
595
+ return { ok: true, payload: isHostMessageSendAnswer2(answer) ? answer : { status: "delivery_uncertain", reason: "the host's message handler returned a malformed answer" } };
596
+ } catch (err) {
597
+ console.error(`winter: hostMessaging.send threw (${err instanceof Error ? err.name : "error"}) -- answering delivery_uncertain`);
598
+ return { ok: true, payload: { status: "delivery_uncertain", reason: "the host's message handler failed" } };
599
+ } finally {
600
+ dispose();
601
+ }
602
+ };
603
+ }
604
+ function makeHostMessageListHandler(host, abortController) {
605
+ return async (payload, handlerCtx) => {
606
+ if (!isHostMessageListRequest2(payload))
607
+ return { ok: false, error: { code: "invalid_payload", message: "host_message_list expects { fromAgentId? }" } };
608
+ const { controller, dispose } = abortableFor(abortController, handlerCtx);
609
+ try {
610
+ const answer = await host.list({ ...payload?.fromAgentId !== undefined ? { fromAgentId: payload.fromAgentId } : {} }, { signal: controller.signal });
611
+ return { ok: true, payload: normaliseHostMessageListAnswer2(answer) ?? { sessions: [] } };
612
+ } catch (err) {
613
+ console.error(`winter: hostMessaging.list threw (${err instanceof Error ? err.name : "error"}) -- answering an empty listing`);
614
+ return { ok: true, payload: { sessions: [] } };
615
+ } finally {
616
+ dispose();
617
+ }
618
+ };
619
+ }
620
+ function makeHostSessionStopHandler(host, abortController) {
621
+ return async (payload, handlerCtx) => {
622
+ if (!isHostSessionStopRequest2(payload))
623
+ return { ok: false, error: { code: "invalid_payload", message: "host_session_stop expects { id, fromAgentId? }" } };
624
+ if (host.stop === undefined)
625
+ return { ok: true, payload: { status: "not_found", reason: `no task or session "${payload.id}" is known` } };
626
+ const { controller, dispose } = abortableFor(abortController, handlerCtx);
627
+ try {
628
+ const answer = await host.stop({ id: payload.id, ...payload.fromAgentId !== undefined ? { fromAgentId: payload.fromAgentId } : {} }, { signal: controller.signal });
629
+ return { ok: true, payload: isHostSessionStopAnswer2(answer) ? answer : { status: "unavailable", reason: "the host's stop handler returned a malformed answer" } };
630
+ } catch (err) {
631
+ console.error(`winter: hostMessaging.stop threw (${err instanceof Error ? err.name : "error"}) -- answering unavailable`);
632
+ return { ok: true, payload: { status: "unavailable", reason: "the host's stop handler failed" } };
633
+ } finally {
634
+ dispose();
635
+ }
636
+ };
637
+ }
554
638
  function buildRuntimeHooksConfig(hooks) {
555
639
  if (!hooks)
556
640
  return;
@@ -735,6 +819,7 @@ function query(args) {
735
819
  ...options.maxOutputTokens !== undefined ? { maxOutputTokens: options.maxOutputTokens } : {},
736
820
  ...brand.keychainService !== WINTER_BRAND2.keychainService || options.keychainService !== undefined ? { keychainService: brand.keychainService } : {},
737
821
  ...options.onCredentialResolve !== undefined ? { hostCredentials: true } : {},
822
+ ...options.hostMessaging !== undefined ? { hostMessaging: true } : {},
738
823
  ...options.autoClassifier !== undefined ? { autoClassifier: options.autoClassifier } : {},
739
824
  ...options.advisor !== undefined ? { advisor: options.advisor } : {},
740
825
  ...options.web !== undefined ? { web: options.web } : {},
@@ -812,7 +897,7 @@ function query(args) {
812
897
  incomingRequestAborts.set(cf.requestId, cancel);
813
898
  try {
814
899
  const result = await handler(cf.payload, { signal: cancel.signal });
815
- if (cancel.signal.aborted)
900
+ if (cancel.signal.aborted && !ANSWERED_AFTER_CANCEL.has(cf.subtype))
816
901
  return;
817
902
  if (result.ok) {
818
903
  writeControlResponse({
@@ -825,7 +910,7 @@ function query(args) {
825
910
  writeControlResponse({ type: "control_response", requestId: cf.requestId, ok: false, error: result.error });
826
911
  }
827
912
  } catch (err) {
828
- if (cancel.signal.aborted)
913
+ if (cancel.signal.aborted && !ANSWERED_AFTER_CANCEL.has(cf.subtype))
829
914
  return;
830
915
  const message = err instanceof Error ? err.message : String(err);
831
916
  writeControlResponse({ type: "control_response", requestId: cf.requestId, ok: false, error: { code: "handler_threw", message } });
@@ -897,6 +982,11 @@ function query(args) {
897
982
  if (options.onCredentialResolve) {
898
983
  controlRequestHandlers.set(CREDENTIAL_RESOLVE_SUBTYPE, makeCredentialResolveHandler(options.onCredentialResolve, options.abortController));
899
984
  }
985
+ if (options.hostMessaging) {
986
+ controlRequestHandlers.set(HOST_MESSAGE_SEND_SUBTYPE, makeHostMessageSendHandler(options.hostMessaging, options.abortController));
987
+ controlRequestHandlers.set(HOST_MESSAGE_LIST_SUBTYPE, makeHostMessageListHandler(options.hostMessaging, options.abortController));
988
+ controlRequestHandlers.set(HOST_SESSION_STOP_SUBTYPE, makeHostSessionStopHandler(options.hostMessaging, options.abortController));
989
+ }
900
990
  if (options.onMcpOAuthRefresh) {
901
991
  controlRequestHandlers.set(MCP_OAUTH_REFRESH_SUBTYPE, makeMcpOAuthRefreshHandler(options.onMcpOAuthRefresh, options.abortController));
902
992
  }
@@ -1180,7 +1270,7 @@ function withTestKeychainRedirect(env) {
1180
1270
  return { ...env, [TEST_KEYCHAIN_ENV]: redirect };
1181
1271
  }
1182
1272
  // src/version.ts
1183
- var SDK_VERSION = "0.0.38";
1273
+ var SDK_VERSION = "0.0.40";
1184
1274
  // src/paths/project-key.ts
1185
1275
  var TRANSCRIPT_PROJECT_KEY_MAX_LENGTH = 64;
1186
1276
  var VENDOR_PROJECT_KEY_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
@@ -2356,6 +2446,9 @@ export {
2356
2446
  ESCALATING_PERMISSION_MODES,
2357
2447
  FIRST_PARTY_ORIGINATORS2 as FIRST_PARTY_ORIGINATORS,
2358
2448
  HOOK_EVENTS,
2449
+ HOST_MESSAGE_LIST_SUBTYPE,
2450
+ HOST_MESSAGE_SEND_SUBTYPE,
2451
+ HOST_SESSION_STOP_SUBTYPE,
2359
2452
  InvalidBrandError,
2360
2453
  MCP_OAUTH_REFRESH_SUBTYPE,
2361
2454
  MESSAGING_CONTROL_SUBTYPES,
@@ -0,0 +1,42 @@
1
+ import type { HostMessageListAnswer, HostMessageListRequest, HostMessageSendAnswer, HostMessageSendRequest, HostReachableSession, HostSessionStopAnswer, HostSessionStopRequest } from "../protocol/config.js";
2
+ import type { DeliveryOutcome, ListedRuntimeObject } from "./adapter.js";
3
+ /**
4
+ * What the router core (and `TaskStop`) need from a host: `Options.hostMessaging`, already bound to one
5
+ * session. `signal` is the calling tool's own: an interrupted sender cancels the request, and the host
6
+ * is told (its handler's `signal` aborts) so it can avoid delivering.
7
+ */
8
+ export interface HostMessagingPort {
9
+ send(request: HostMessageSendRequest, opts?: {
10
+ signal?: AbortSignal;
11
+ }): Promise<HostMessageSendAnswer>;
12
+ list(request: HostMessageListRequest, opts?: {
13
+ signal?: AbortSignal;
14
+ }): Promise<HostMessageListAnswer>;
15
+ stop(request: HostSessionStopRequest, opts?: {
16
+ signal?: AbortSignal;
17
+ }): Promise<HostSessionStopAnswer>;
18
+ }
19
+ /** A host's sentence for the model is bounded: it is rendered into a tool result. */
20
+ export declare const HOST_MESSAGE_NOTE_MAX = 500;
21
+ /** A host listing is bounded: it is rendered into a tool result, one line per row. */
22
+ export declare const HOST_MESSAGE_LIST_MAX = 200;
23
+ /** The request shape, checked on the WRAPPER side before the host's callback sees it. */
24
+ export declare function isHostMessageSendRequest(payload: unknown): payload is HostMessageSendRequest;
25
+ export declare function isHostMessageListRequest(payload: unknown): payload is HostMessageListRequest;
26
+ export declare function isHostSessionStopRequest(payload: unknown): payload is HostSessionStopRequest;
27
+ /** A well-formed stop answer: a known status, a non-empty reason wherever one is required. */
28
+ export declare function isHostSessionStopAnswer(value: unknown): value is HostSessionStopAnswer;
29
+ /** A well-formed send answer: a known status, a reason wherever one is required, the optional facts typed. */
30
+ export declare function isHostMessageSendAnswer(value: unknown): value is HostMessageSendAnswer;
31
+ /**
32
+ * A list answer, normalised: every malformed row is DROPPED (one bad row must not hide the others, and
33
+ * an unaddressable row would advertise a target the model cannot name), and the listing is capped.
34
+ * `undefined` for an answer that is not a listing at all.
35
+ */
36
+ export declare function normaliseHostMessageListAnswer(value: unknown): HostMessageListAnswer | undefined;
37
+ /** A host's session row as a ListAgents row: always a `session` of the Winter runtime, never resumable "for free" or idle-notifiable unless the host says so through SendMessage itself. */
38
+ export declare function hostSessionToListed(row: HostReachableSession): ListedRuntimeObject;
39
+ /** The host's answer as the router's outcome, under the RUNTIME's message id. Assumes `isHostMessageSendAnswer`. */
40
+ export declare function hostAnswerToOutcome(messageId: string, answer: HostMessageSendAnswer): DeliveryOutcome;
41
+ /** A host note, cut to its bound (it is model-visible). Empty and absent read the same. */
42
+ export declare function boundedHostNote(note: string | undefined): string | undefined;
@@ -11,5 +11,8 @@ export type { PermissionClassLabel, CrossSessionInbound, InboundDecisionParams,
11
11
  export { isIdleSubscribeSenderAllowed, isIdleSubscribeTargetAllowed, createNotificationQueue, createIdleSubscriptionStore, } from "./idle.js";
12
12
  export type { NotificationRecord, NotificationQueue, PendingIdleSubscription, IdleSubscriptionStore } from "./idle.js";
13
13
  export { AGENT_MESSAGE_TAG, RESERVED_NOTIFICATION_KEY_PREFIX, escapeAttributionText, escapeAttributionAttribute, facetNotificationKey, isReservedNotificationKey } from "./attribution.js";
14
+ export { HOST_MESSAGE_NOTE_MAX, HOST_MESSAGE_LIST_MAX, isHostMessageSendRequest, isHostMessageListRequest, isHostMessageSendAnswer, isHostSessionStopRequest, isHostSessionStopAnswer, normaliseHostMessageListAnswer, hostSessionToListed, hostAnswerToOutcome, boundedHostNote, } from "./host.js";
15
+ export type { HostMessagingPort } from "./host.js";
16
+ export type { HostMessageSendRequest, HostMessageSendAnswer, HostMessageListRequest, HostMessageListAnswer, HostReachableSession, HostSessionStopRequest, HostSessionStopAnswer } from "../protocol/config.js";
14
17
  export { MAX_TRACKED_MESSAGE_IDS, rememberBounded, createMessagingRouterSeam, createSubscriberDirectory, callerAddress, sendMessage, listAgents, readNotifications, createMessagingRouter, } from "./router.js";
15
18
  export type { MessagingRouterSeamWithRoster, SubscriberDirectory, MessagingRuntimeDeps, CallerContext, SessionCallerContext, SendMessageInput, NotifyOutcome, SendMessageResult, ListAgentsInput, MessagingRouter, } from "./router.js";
@@ -7,6 +7,17 @@ import {
7
7
  buildChildAddress2,
8
8
  parseRuntimeAddress2,
9
9
  sameAddress2,
10
+ HOST_MESSAGE_NOTE_MAX2,
11
+ HOST_MESSAGE_LIST_MAX2,
12
+ isHostMessageSendRequest2,
13
+ isHostMessageListRequest2,
14
+ isHostSessionStopRequest2,
15
+ isHostSessionStopAnswer2,
16
+ isHostMessageSendAnswer2,
17
+ normaliseHostMessageListAnswer2,
18
+ hostSessionToListed2,
19
+ hostAnswerToOutcome2,
20
+ boundedHostNote2,
10
21
  MAX_GLOBAL_MESSAGE_SIZE2,
11
22
  DEFAULT_MESSAGE_TTL_MS2,
12
23
  MAX_HOP_COUNT2,
@@ -56,13 +67,15 @@ import {
56
67
  listAgents2,
57
68
  readNotifications2,
58
69
  createMessagingRouter2
59
- } from "../index-51ysrfm8.js";
70
+ } from "../index-3zhkc4nb.js";
60
71
  export {
61
72
  ACCEPTED_QUEUE_CAP2 as ACCEPTED_QUEUE_CAP,
62
73
  AGENT_MESSAGE_TAG2 as AGENT_MESSAGE_TAG,
63
74
  DEFAULT_HOLD_EXPIRY_MS2 as DEFAULT_HOLD_EXPIRY_MS,
64
75
  DEFAULT_MESSAGE_TTL_MS2 as DEFAULT_MESSAGE_TTL_MS,
65
76
  HELD_INBOX_CAP2 as HELD_INBOX_CAP,
77
+ HOST_MESSAGE_LIST_MAX2 as HOST_MESSAGE_LIST_MAX,
78
+ HOST_MESSAGE_NOTE_MAX2 as HOST_MESSAGE_NOTE_MAX,
66
79
  MAX_GLOBAL_MESSAGE_SIZE2 as MAX_GLOBAL_MESSAGE_SIZE,
67
80
  MAX_HOP_COUNT2 as MAX_HOP_COUNT,
68
81
  MAX_TRACKED_MESSAGE_IDS2 as MAX_TRACKED_MESSAGE_IDS,
@@ -71,6 +84,7 @@ export {
71
84
  REFERENCE_RUNTIME_KIND2 as REFERENCE_RUNTIME_KIND,
72
85
  RESERVED_NOTIFICATION_KEY_PREFIX2 as RESERVED_NOTIFICATION_KEY_PREFIX,
73
86
  ambiguous2 as ambiguous,
87
+ boundedHostNote2 as boundedHostNote,
74
88
  buildChildAddress2 as buildChildAddress,
75
89
  buildDefaultHoldEntry2 as buildDefaultHoldEntry,
76
90
  buildExplicitHoldEntry2 as buildExplicitHoldEntry,
@@ -94,12 +108,20 @@ export {
94
108
  facetNotificationKey2 as facetNotificationKey,
95
109
  held2 as held,
96
110
  hopCountExceeded2 as hopCountExceeded,
111
+ hostAnswerToOutcome2 as hostAnswerToOutcome,
112
+ hostSessionToListed2 as hostSessionToListed,
113
+ isHostMessageListRequest2 as isHostMessageListRequest,
114
+ isHostMessageSendAnswer2 as isHostMessageSendAnswer,
115
+ isHostMessageSendRequest2 as isHostMessageSendRequest,
116
+ isHostSessionStopAnswer2 as isHostSessionStopAnswer,
117
+ isHostSessionStopRequest2 as isHostSessionStopRequest,
97
118
  isIdleSubscribeSenderAllowed2 as isIdleSubscribeSenderAllowed,
98
119
  isIdleSubscribeTargetAllowed2 as isIdleSubscribeTargetAllowed,
99
120
  isReservedNotificationKey2 as isReservedNotificationKey,
100
121
  listAgents2 as listAgents,
101
122
  mapFromModeToPermissionClass2 as mapFromModeToPermissionClass,
102
123
  messageExceedsMaxSize2 as messageExceedsMaxSize,
124
+ normaliseHostMessageListAnswer2 as normaliseHostMessageListAnswer,
103
125
  notFound2 as notFound,
104
126
  parseRuntimeAddress2 as parseRuntimeAddress,
105
127
  queued2 as queued,
@@ -1,4 +1,5 @@
1
1
  import { type RuntimeAddress, type ListedRuntimeObject, type DeliveryOutcome, type RuntimeMessagingAdapter, type MessagingRouterSeam, type ChildLike } from "./adapter.js";
2
+ import { type HostMessagingPort } from "./host.js";
2
3
  import type { NotificationQueue, NotificationRecord } from "./idle.js";
3
4
  import { type LoopGuard } from "./outcomes.js";
4
5
  export interface MessagingRouterSeamWithRoster extends MessagingRouterSeam {
@@ -34,6 +35,14 @@ export interface MessagingRuntimeDeps {
34
35
  * "everything is uncertain", which is exactly the conservative reading.
35
36
  */
36
37
  classifyDeliveryError?(err: unknown): "refused" | "uncertain";
38
+ /**
39
+ * Host messaging (`Options.hostMessaging`): the port to the HOST's other sessions for the OWNING
40
+ * session `owningSessionId` (a subagent's calls use its parent's, since they share the session id),
41
+ * or `undefined` when that session has none -- which is every session of a standalone SDK user, and
42
+ * every session of a host that composes this core itself (the router package). Consulted only after
43
+ * in-process resolution answered `not_found` (SendMessage) and to add rows (ListAgents).
44
+ */
45
+ hostMessaging?(owningSessionId: string): HostMessagingPort | undefined;
37
46
  }
38
47
  export interface CallerContext {
39
48
  sessionId: string;
@@ -57,8 +66,11 @@ export interface NotifyOutcome {
57
66
  export interface SendMessageResult {
58
67
  outcome: DeliveryOutcome;
59
68
  notify?: NotifyOutcome;
69
+ note?: string;
60
70
  }
61
- export declare function sendMessage(deps: MessagingRuntimeDeps, caller: CallerContext, input: SendMessageInput): Promise<SendMessageResult>;
71
+ export declare function sendMessage(deps: MessagingRuntimeDeps, caller: CallerContext, input: SendMessageInput, opts?: {
72
+ signal?: AbortSignal;
73
+ }): Promise<SendMessageResult>;
62
74
  export interface ListAgentsInput {
63
75
  channel?: string;
64
76
  q?: string;
@@ -76,9 +88,14 @@ export declare function formatListing(rows: readonly ListedRuntimeObject[]): str
76
88
  export interface SessionCallerContext {
77
89
  sessionId: string;
78
90
  }
79
- export declare function listAgents(deps: MessagingRuntimeDeps, caller: SessionCallerContext, _input: ListAgentsInput): Promise<{
91
+ /** The line a listing ends with when the host left reachable sessions out (its cap), so nothing is silently cut. */
92
+ export declare function omittedLine(omitted: number): string;
93
+ export declare function listAgents(deps: MessagingRuntimeDeps, caller: SessionCallerContext, _input: ListAgentsInput, opts?: {
94
+ signal?: AbortSignal;
95
+ }): Promise<{
80
96
  listing: string;
81
97
  rows: ListedRuntimeObject[];
98
+ omitted?: number;
82
99
  }>;
83
100
  export declare function readNotifications(deps: MessagingRuntimeDeps, caller: SessionCallerContext): {
84
101
  notifications: NotificationRecord[];
@@ -90,6 +107,7 @@ export interface MessagingRouter {
90
107
  listAgents(caller: SessionCallerContext, input: ListAgentsInput): Promise<{
91
108
  listing: string;
92
109
  rows: ListedRuntimeObject[];
110
+ omitted?: number;
93
111
  }>;
94
112
  readNotifications(caller: SessionCallerContext): {
95
113
  notifications: NotificationRecord[];
package/dist/options.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { SpawnClaudeCodeProcess } from "./transport.js";
2
2
  import type { PermissionMode, CanUseTool, HookEvent, HookCallbackMatcher } from "./permissions/types.js";
3
- import type { SandboxSettingsConfig, McpServerToolPolicy, McpStdioServerConfig, McpHttpServerConfig, McpSSEServerConfig, McpSdkServerConfig, RuntimeAgentDefinition, SdkPluginConfig, SystemPromptOption, OutputFormat, SkillsOption, ProviderSelection, ThinkingConfig, EffortLevel, AutoClassifierConfig, AdvisorConfig, WebToolsConfig, WebPrivateAddressPolicy, AutoMemoryConfig, CredentialRef, McpOAuthRefreshRequest, McpOAuthRefreshAnswer, CredentialResolveRequest, CredentialResolveAnswer } from "./protocol/config.js";
3
+ import type { SandboxSettingsConfig, McpServerToolPolicy, McpStdioServerConfig, McpHttpServerConfig, McpSSEServerConfig, McpSdkServerConfig, RuntimeAgentDefinition, SdkPluginConfig, SystemPromptOption, OutputFormat, SkillsOption, ProviderSelection, ThinkingConfig, EffortLevel, AutoClassifierConfig, AdvisorConfig, WebToolsConfig, WebPrivateAddressPolicy, AutoMemoryConfig, CredentialRef, McpOAuthRefreshRequest, McpOAuthRefreshAnswer, CredentialResolveRequest, CredentialResolveAnswer, HostMessageSendRequest, HostMessageSendAnswer, HostMessageListRequest, HostMessageListAnswer, HostSessionStopRequest, HostSessionStopAnswer } from "./protocol/config.js";
4
4
  import type { SettingSource } from "./settings/types.js";
5
5
  import type { SessionStore } from "./store/session-store.js";
6
6
  import { type BrandProfile } from "./brand.js";
@@ -45,6 +45,33 @@ export declare const TEST_KEYCHAIN_MEMORY = "memory";
45
45
  export declare const MCP_OAUTH_REFRESH_SUBTYPE = "mcp_oauth_refresh";
46
46
  /** WS-25 §7: the runtime -> host control subtype a host-brokered session resolves a Keychain credential with (`CredentialResolveRequest` -> `CredentialResolveAnswer`). */
47
47
  export declare const CREDENTIAL_RESOLVE_SUBTYPE = "credential_resolve";
48
+ /** Host messaging: the runtime -> host control subtype `SendMessage` asks its host to deliver with (`HostMessageSendRequest` -> `HostMessageSendAnswer`). */
49
+ export declare const HOST_MESSAGE_SEND_SUBTYPE = "host_message_send";
50
+ /** Host messaging: the runtime -> host control subtype `ListAgents` asks its host what it can reach with (`HostMessageListRequest` -> `HostMessageListAnswer`). */
51
+ export declare const HOST_MESSAGE_LIST_SUBTYPE = "host_message_list";
52
+ /** Host messaging: the runtime -> host control subtype `TaskStop` asks its host to stop one of the host's sessions with (`HostSessionStopRequest` -> `HostSessionStopAnswer`). */
53
+ export declare const HOST_SESSION_STOP_SUBTYPE = "host_session_stop";
54
+ /**
55
+ * Host messaging, WINTER-ONLY: the handler a host that runs MANY sessions gives one session so its
56
+ * `SendMessage` and `ListAgents` reach the host's other sessions. See `Options.hostMessaging`.
57
+ */
58
+ export interface HostMessagingHandler {
59
+ /** Deliver one message this session's `SendMessage` could not resolve in-process. Never throws for a policy answer -- refuse with a typed status. */
60
+ send(request: HostMessageSendRequest, options: {
61
+ signal: AbortSignal;
62
+ }): Promise<HostMessageSendAnswer>;
63
+ /** The sessions this session can reach through the host, for `ListAgents` (beside its own subagents). */
64
+ list(request: HostMessageListRequest, options: {
65
+ signal: AbortSignal;
66
+ }): Promise<HostMessageListAnswer>;
67
+ /**
68
+ * Stop (interrupt the running turn of) one of the host's sessions, for a `TaskStop` whose `task_id`
69
+ * names no task of this session. Optional: a host without it answers every such request `not_found`.
70
+ */
71
+ stop?(request: HostSessionStopRequest, options: {
72
+ signal: AbortSignal;
73
+ }): Promise<HostSessionStopAnswer>;
74
+ }
48
75
  /**
49
76
  * Every web-tool default, spelled ONCE. A reader function resolves an absent field against this --
50
77
  * no consumer writes a literal of its own.
@@ -101,7 +128,16 @@ export interface WinterMcpServerInstance {
101
128
  };
102
129
  _meta?: Record<string, unknown>;
103
130
  }>;
104
- callTool(name: string, args: Record<string, unknown>): Promise<{
131
+ /**
132
+ * `extra.signal` (SDK 0.0.40) is ABORTED when the runtime cancels the call (an interrupted turn sends
133
+ * `control_cancel_request`). A tool that holds an exclusive resource should stop promptly and then return:
134
+ * the runtime keeps the tool's concurrency lane held until this promise settles (or a bounded grace runs
135
+ * out), so the next call of that lane never overlaps a tool still running. Optional: an instance that
136
+ * ignores it behaves as before.
137
+ */
138
+ callTool(name: string, args: Record<string, unknown>, extra?: {
139
+ signal?: AbortSignal;
140
+ }): Promise<{
105
141
  content: unknown[];
106
142
  isError?: boolean;
107
143
  }>;
@@ -256,6 +292,29 @@ export interface Options {
256
292
  onCredentialResolve?: (request: CredentialResolveRequest, options: {
257
293
  signal: AbortSignal;
258
294
  }) => Promise<CredentialResolveAnswer>;
295
+ /**
296
+ * Host messaging, WINTER-ONLY: this session's line to the host's OTHER sessions. When set, `query()`
297
+ * puts `hostMessaging: true` on the wire and answers the runtime's `host_message_send` /
298
+ * `host_message_list` control requests with it -- identically for a spawned `winter` process and an
299
+ * embedded Worker, since both speak the same frame stream to this wrapper.
300
+ *
301
+ * - `SendMessage` resolves `to` in-process first (this session's subagents, then the in-process peer
302
+ * directory). Only a `not_found` there is handed to `send`; a subagent, a self-target, a stale or an
303
+ * ambiguous name never reaches the host. The host's typed answer is the tool's result, under the
304
+ * runtime's own message id (so a retry of the same tool call short-circuits on the stored outcome).
305
+ * - `ListAgents` lists this session's subagents and then whatever `list` returns, as `session` rows
306
+ * (with the host's `omitted` count, so nothing is silently cut).
307
+ * - `TaskStop` with a `task_id` that names no task of this session is handed to `stop` (when given):
308
+ * the host interrupts that session's running turn.
309
+ *
310
+ * The handler is per session: it knows its caller by construction, and the request never names the
311
+ * sender (only `fromAgentId`, information about which subagent of THIS session asked). A throwing
312
+ * `send` answers `delivery_uncertain`; a throwing `list` lists nothing.
313
+ *
314
+ * Absent: `SendMessage` and `ListAgents` reach exactly what the session's own process holds, as before.
315
+ * Never serialized (a JS object of functions).
316
+ */
317
+ hostMessaging?: HostMessagingHandler;
259
318
  systemPrompt?: SystemPromptOption;
260
319
  plugins?: SdkPluginConfig[];
261
320
  skills?: SkillsOption;
@@ -231,6 +231,100 @@ export type CredentialResolveAnswer = {
231
231
  ok: false;
232
232
  reason: "not_found" | "not_allowed" | "stale" | "unavailable";
233
233
  };
234
+ /**
235
+ * Host messaging, WINTER-ONLY: the runtime -> host `host_message_send` control request (subtype
236
+ * `HOST_MESSAGE_SEND_SUBTYPE`), sent only when `RuntimeConfig.hostMessaging` is set.
237
+ *
238
+ * WHY. A session's `SendMessage` resolves `to` against what its own process can reach: the session's
239
+ * subagents and the in-process peer directory, which holds the calling session alone. A HOST that runs
240
+ * many sessions (the Winter daemon: one process or one Worker per session) owns every other one, so the
241
+ * runtime asks it when -- and only when -- local resolution finds nothing (`not_found`). Subagents,
242
+ * self-target, a stale or ambiguous name are all still answered in-process and never reach the host.
243
+ *
244
+ * - `to` -- EXACTLY what the model wrote (a host session id, a `session:<id>` address from ListAgents,
245
+ * a name the host recognises). Resolution is the host's.
246
+ * - `messageId` -- the runtime's own id for this call, stable across a retry of the same tool call (the
247
+ * runtime's ledger short-circuits a retry before it ever asks again; the host MAY also dedupe on it).
248
+ * - `fromAgentId` -- set when one of this session's SUBAGENTS is the sender. Information only: the host
249
+ * knows which session is asking from which session's handler answered, and must never take the sender
250
+ * from the payload.
251
+ */
252
+ export interface HostMessageSendRequest {
253
+ to: string;
254
+ message: string;
255
+ summary?: string;
256
+ notifyWhenIdle?: boolean;
257
+ messageId: string;
258
+ fromAgentId?: string;
259
+ }
260
+ /**
261
+ * The host's answer. `status` is one of the delivery outcomes a host can honestly report; the runtime
262
+ * stamps its own `messageId` on it and records it in its ledger.
263
+ *
264
+ * - `delivered` (the target is reading it now), `queued` (behind a turn already running),
265
+ * `resumed_and_delivered` (the target was not running and the host resumed it for this message);
266
+ * - `refused` / `not_found` -- `reason` required; `unavailable` -- `reason` required, `retryable`
267
+ * optional (default `false`); `delivery_uncertain` -- `reason` required (the effect may have happened).
268
+ *
269
+ * `notify` is the separate fact of whether `notifyWhenIdle` was honoured, rendered beside the outcome
270
+ * exactly like the in-process router's combined call. `note` is ONE short sentence for the model beside
271
+ * the outcome (e.g. whether it will be told when the target finishes) -- never a second outcome.
272
+ */
273
+ export interface HostMessageSendAnswer {
274
+ status: "delivered" | "queued" | "resumed_and_delivered" | "refused" | "not_found" | "unavailable" | "delivery_uncertain";
275
+ reason?: string;
276
+ retryable?: boolean;
277
+ notify?: {
278
+ subscribed?: true;
279
+ refused?: string;
280
+ };
281
+ note?: string;
282
+ }
283
+ /** `host_message_list` (subtype `HOST_MESSAGE_LIST_SUBTYPE`): what this session can reach through its host, for `ListAgents`. */
284
+ export interface HostMessageListRequest {
285
+ /** As on `HostMessageSendRequest`: set when a subagent is asking. Information only. */
286
+ fromAgentId?: string;
287
+ }
288
+ /**
289
+ * One session the host lists. `address` is what the model passes back as `to` (it must satisfy
290
+ * `SendMessage`'s own `to` rules: non-empty, at most 300 characters, no newline, no `*`); `name` is a
291
+ * display name (a title). The runtime renders each as an ordinary `session` row of ListAgents.
292
+ */
293
+ export interface HostReachableSession {
294
+ address: string;
295
+ name?: string;
296
+ status: "starting" | "running" | "idle" | "exited" | "unavailable" | "archived";
297
+ /** The session's product mode (`code`, `dispatch`, …). */
298
+ mode: string;
299
+ cwd?: string;
300
+ }
301
+ export interface HostMessageListAnswer {
302
+ sessions: HostReachableSession[];
303
+ /** How many MORE reachable sessions the host did not list (its own cap). Rendered as a count, so a listing is never silently truncated. */
304
+ omitted?: number;
305
+ }
306
+ /**
307
+ * Host messaging: the runtime -> host `host_session_stop` control request (subtype
308
+ * `HOST_SESSION_STOP_SUBTYPE`). `TaskStop` sends it when its `task_id` names no task of this session (a
309
+ * background command, a subagent, a workflow) -- i.e. when the id may be one of the HOST's sessions.
310
+ * Stopping a session interrupts the turn it is running; it never deletes or archives anything.
311
+ * `fromAgentId` as on `HostMessageSendRequest`: information only.
312
+ */
313
+ export interface HostSessionStopRequest {
314
+ id: string;
315
+ fromAgentId?: string;
316
+ }
317
+ /**
318
+ * `stopped` (a running turn was interrupted) and `not_running` (nothing to stop) are ordinary answers;
319
+ * `refused`, `not_found` and `unavailable` carry a required `reason` and reach the model as errors.
320
+ */
321
+ export interface HostSessionStopAnswer {
322
+ status: "stopped" | "not_running" | "refused" | "not_found" | "unavailable";
323
+ reason?: string;
324
+ /** One sentence for the model beside a `stopped` / `not_running` result — e.g. what happens to messages
325
+ * that were queued behind the interrupted turn. Rendered into TaskStop's `message`. */
326
+ note?: string;
327
+ }
234
328
  export interface McpStdioServerConfig {
235
329
  type?: "stdio";
236
330
  command: string;
@@ -301,6 +395,17 @@ export interface McpSdkServerConfig {
301
395
  * host owns it.
302
396
  */
303
397
  toolNames?: Record<string, string>;
398
+ /**
399
+ * SDK 0.0.40, a Winter extension: CONCURRENCY LANES for some of this server's tools, keyed by the
400
+ * server's own tool name (`{ computer: "computer", browser: "browser" }`). A round's calls normally run
401
+ * one at a time unless the tool is read-only (`readOnlyHint: true`); a tool in a lane instead runs BESIDE
402
+ * the round's other calls, but never beside another call of the SAME lane -- calls of one lane run one at
403
+ * a time, in call order, across the round. Use it for a tool that holds one exclusive resource (a screen,
404
+ * a browser) yet is safe next to everything else. A call that is neither read-only nor in a lane is a
405
+ * barrier, exactly as before. A lane key is 1-64 characters of `[A-Za-z0-9_.:-]`; anything else is
406
+ * ignored. Only an in-process (`sdk`) server may declare lanes; the host vouches for its own tools.
407
+ */
408
+ toolLanes?: Record<string, string>;
304
409
  }
305
410
  export type McpServerConfigForProcessTransport = McpStdioServerConfig | McpHttpServerConfig | McpSSEServerConfig | McpSdkServerConfig;
306
411
  export type AgentMcpServerSpec = string | Record<string, McpServerConfigForProcessTransport>;
@@ -538,6 +643,13 @@ export interface RuntimeConfig {
538
643
  * runtime's own Keychain store, as before (a standalone SDK user). A flag, never material.
539
644
  */
540
645
  hostCredentials?: boolean;
646
+ /**
647
+ * Host messaging, WINTER-ONLY: `true` when the host answers `host_message_send` / `host_message_list`
648
+ * (`Options.hostMessaging`). `SendMessage` then asks the host to deliver whatever it cannot resolve
649
+ * in-process, and `ListAgents` adds the sessions the host lists. Absent: in-process only, as before.
650
+ * A flag, never a handler.
651
+ */
652
+ hostMessaging?: boolean;
541
653
  autoClassifier?: AutoClassifierConfig;
542
654
  advisor?: AdvisorConfig;
543
655
  /** The wire twin of `Options.web` -- see `WebToolsConfig`. Pure passthrough; absent means every default in `WEB_TOOLS_DEFAULTS`. */
@@ -3,8 +3,9 @@ import {
3
3
  callerAddress2,
4
4
  sendMessage2,
5
5
  formatListing,
6
+ omittedLine,
6
7
  listAgents2
7
- } from "../index-51ysrfm8.js";
8
+ } from "../index-3zhkc4nb.js";
8
9
  import {
9
10
  resolveWinterHome2,
10
11
  WinterCompatibilitySessionStore2
@@ -220,12 +221,16 @@ function messagingToolPortFromRuntimeDeps(deps) {
220
221
  message: request.body,
221
222
  ...request.summary === undefined ? {} : { summary: request.summary },
222
223
  ...request.notifyWhenIdle === undefined ? {} : { notify_when_idle: request.notifyWhenIdle }
223
- });
224
+ }, request.signal !== undefined ? { signal: request.signal } : {});
224
225
  },
225
226
  async listReachable(scope) {
226
227
  const { rows } = await listAgents2(deps, { sessionId: scope.from.parentWinterSessionId ?? scope.from.winterSessionId }, {});
227
228
  return rows;
228
229
  },
230
+ async listReachableDetailed(scope) {
231
+ const { rows, omitted } = await listAgents2(deps, { sessionId: scope.from.parentWinterSessionId ?? scope.from.winterSessionId }, {}, scope.signal !== undefined ? { signal: scope.signal } : {});
232
+ return { rows, ...omitted !== undefined ? { omitted } : {} };
233
+ },
229
234
  readNotifications(sessionId) {
230
235
  return deps.notifications.drain(sessionId);
231
236
  }
@@ -274,9 +279,10 @@ function createMessagingToolHandlers(port, caller) {
274
279
  body: accepted.args.message,
275
280
  ...summary === undefined ? {} : { summary },
276
281
  ...accepted.args.notify_when_idle === undefined ? {} : { notifyWhenIdle: accepted.args.notify_when_idle },
277
- ...who.toolUseId === undefined ? {} : { originToolCallId: who.toolUseId }
282
+ ...who.toolUseId === undefined ? {} : { originToolCallId: who.toolUseId },
283
+ ...who.signal === undefined ? {} : { signal: who.signal }
278
284
  });
279
- const payload = result.notify === undefined ? result.outcome : { ...result.outcome, notify: result.notify };
285
+ const payload = { ...result.outcome, ...result.notify === undefined ? {} : { notify: result.notify }, ...result.note === undefined ? {} : { note: result.note } };
280
286
  return text(JSON.stringify(payload), MODEL_FACING_FAILURES.has(result.outcome.status));
281
287
  });
282
288
  },
@@ -285,8 +291,13 @@ function createMessagingToolHandlers(port, caller) {
285
291
  if (!accepted.ok)
286
292
  return text(accepted.reason, true);
287
293
  return guarded("ListAgents could not reach the messaging system", async () => {
288
- const rows = await port.listReachable({ from: callerAddress2(identity()) });
289
- return text(JSON.stringify({ listing: formatListing(rows) }));
294
+ const who = identity();
295
+ const from = callerAddress2(who);
296
+ const detailed = port.listReachableDetailed !== undefined ? await port.listReachableDetailed({ from, ...who.signal !== undefined ? { signal: who.signal } : {} }) : { rows: await port.listReachable({ from }) };
297
+ const rows = detailed.rows;
298
+ const omitted = detailed.omitted ?? 0;
299
+ return text(JSON.stringify({ listing: omitted > 0 ? `${formatListing(rows)}
300
+ ${omittedLine(omitted)}` : formatListing(rows) }));
290
301
  });
291
302
  },
292
303
  async readNotifications(rawArgs) {
@@ -15,6 +15,8 @@ export interface WinterToolCaller {
15
15
  sessionId: string;
16
16
  agentId?: string;
17
17
  toolUseId?: string;
18
+ /** The calling tool's own abort signal, when the host binds the caller per call (host messaging cancels a pending delivery with it). */
19
+ signal?: AbortSignal;
18
20
  }
19
21
  /** The host-neutral tool result: one text body, plus whether the model should read it as a failure. */
20
22
  export interface WinterToolResult {
@@ -1,5 +1,6 @@
1
1
  import { callerAddress, type ListedRuntimeObject, type MessagingRuntimeDeps, type NotificationRecord, type RuntimeAddress, type SendMessageResult } from "../messaging/index.js";
2
2
  export interface MessagingToolPort {
3
+ /** `signal`: the calling tool's own (host messaging cancels a pending host delivery with it). */
3
4
  sendDetailed(request: {
4
5
  from: RuntimeAddress;
5
6
  to: string;
@@ -7,10 +8,19 @@ export interface MessagingToolPort {
7
8
  summary?: string;
8
9
  notifyWhenIdle?: boolean;
9
10
  originToolCallId?: string;
11
+ signal?: AbortSignal;
10
12
  }): Promise<SendMessageResult>;
11
13
  listReachable(scope: {
12
14
  from: RuntimeAddress;
13
15
  }): Promise<ListedRuntimeObject[]>;
16
+ /** Optional: the rows plus how many reachable ones were NOT listed (a host's cap). Preferred by the ListAgents handler when present. */
17
+ listReachableDetailed?(scope: {
18
+ from: RuntimeAddress;
19
+ signal?: AbortSignal;
20
+ }): Promise<{
21
+ rows: ListedRuntimeObject[];
22
+ omitted?: number;
23
+ }>;
14
24
  readNotifications(sessionId: string): {
15
25
  notifications: NotificationRecord[];
16
26
  remaining: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yanlinglabs/winter-agent-sdk",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -45,15 +45,15 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@yanlinglabs/winter-provider-catalog": "0.0.38"
48
+ "@yanlinglabs/winter-provider-catalog": "0.0.40"
49
49
  },
50
50
  "optionalDependencies": {
51
- "@yanlinglabs/winter-agent-sdk-darwin-arm64": "0.0.38"
51
+ "@yanlinglabs/winter-agent-sdk-darwin-arm64": "0.0.40"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^26.4.0",
55
- "@yanlinglabs/winter-agent-runtime": "0.0.38",
56
- "@yanlinglabs/winter-conformance": "0.0.38"
55
+ "@yanlinglabs/winter-agent-runtime": "0.0.40",
56
+ "@yanlinglabs/winter-conformance": "0.0.40"
57
57
  },
58
58
  "scripts": {}
59
59
  }