@botiverse/raft-sdk 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4713,7 +4713,7 @@ const AGENT_API_ROUTE_META = {
4713
4713
  mentionActionsExecute: write,
4714
4714
  taskClaim: write,
4715
4715
  taskList: read,
4716
- taskCreate: write,
4716
+ taskCreate: keyedWrite,
4717
4717
  taskUnclaim: destructive,
4718
4718
  taskAssign: naturalDestructive,
4719
4719
  taskUpdateStatus: naturalDestructive,
@@ -4745,7 +4745,7 @@ const AGENT_API_ROUTE_META = {
4745
4745
  integrationAppLogoUpdate: naturalDestructive,
4746
4746
  integrationAppList: read,
4747
4747
  integrationAppStatus: read,
4748
- actionPrepare: write,
4748
+ actionPrepare: keyedWrite,
4749
4749
  attachmentUpload: write,
4750
4750
  attachmentUploadCapabilities: read,
4751
4751
  attachmentUploadSessionCreate: write,
@@ -4753,6 +4753,7 @@ const AGENT_API_ROUTE_META = {
4753
4753
  attachmentUploadSessionCancel: naturalDestructive,
4754
4754
  attachmentUploadSessionStatus: read,
4755
4755
  attachmentDownload: read,
4756
+ attachmentDownloadUrl: read,
4756
4757
  attachmentCommentsList: read,
4757
4758
  pushWebhookStatus: {
4758
4759
  sideEffect: "read",
@@ -5281,6 +5282,18 @@ const agentApiMessageReactionBodySchema = passthroughObject({ emoji: string().tr
5281
5282
  const agentApiChannelMembershipParamsSchema = passthroughObject({ channelId: string().trim().min(1).transform(asChannelId) });
5282
5283
  const agentApiChannelLifecycleBodySchema = passthroughObject({ target: string().trim().min(1) });
5283
5284
  const agentApiAttachmentDownloadParamsSchema = passthroughObject({ attachmentId: string().trim().min(1) });
5285
+ /**
5286
+ * Short-lived download URL for an attachment visible to the bound agent
5287
+ * credential (`GET /attachments/:attachmentId/url`). For runtimes that cannot
5288
+ * take binary tool results: the agent fetches the bytes itself from `url`
5289
+ * before `expiresAt`. The URL is a bearer capability; never log it.
5290
+ */
5291
+ const agentApiAttachmentDownloadUrlResponseSchema = passthroughObject({
5292
+ url: string().min(1),
5293
+ expiresAt: string().datetime(),
5294
+ filename: string(),
5295
+ mimeType: string()
5296
+ });
5284
5297
  const agentApiAttachmentCommentsParamsSchema = passthroughObject({ attachmentId: string().trim().min(1) });
5285
5298
  const agentApiAttachmentCommentsQuerySchema = passthroughObject({ limit: optionalStringSchema });
5286
5299
  const agentApiAttachmentCommentAnchorSchema = passthroughObject({
@@ -5396,7 +5409,15 @@ const agentApiTaskCreateBodySchema = passthroughObject({
5396
5409
  title: string().trim().min(1),
5397
5410
  creates_resource: boolean().optional()
5398
5411
  })).min(1),
5399
- assignee: string().trim().refine((value) => value.startsWith("@") && value.slice(1).trim().length > 0, { message: "assignee must be an @handle" }).optional()
5412
+ assignee: string().trim().refine((value) => value.startsWith("@") && value.slice(1).trim().length > 0, { message: "assignee must be an @handle" }).optional(),
5413
+ /**
5414
+ * Retry key (same rules as message send's): a repeat with the same key and
5415
+ * the same request replays the first response without creating anything;
5416
+ * the same key with a different request is refused (409
5417
+ * `idempotency_key_reused`). Scoped to the agent and this route, and valid
5418
+ * for 24 hours; after that the key is forgotten and is a new request.
5419
+ */
5420
+ idempotencyKey: optionalStringSchema
5400
5421
  });
5401
5422
  const agentApiTaskUnclaimBodySchema = passthroughObject({
5402
5423
  channel: string().trim().min(1),
@@ -5647,7 +5668,15 @@ const agentApiIntegrationAppStatusQuerySchema = passthroughObject({
5647
5668
  });
5648
5669
  const agentApiActionPrepareBodySchema = passthroughObject({
5649
5670
  target: string().trim().min(1),
5650
- action: actionCardActionSchema
5671
+ action: actionCardActionSchema,
5672
+ /**
5673
+ * Retry key (same rules as message send's): a repeat with the same key and
5674
+ * the same request replays the first response (same card messageId)
5675
+ * without preparing another card; the same key with a different request is
5676
+ * refused (409 `idempotency_key_reused`). Scoped to the agent and this
5677
+ * route, and valid for 24 hours; after that the key is forgotten.
5678
+ */
5679
+ idempotencyKey: optionalStringSchema
5651
5680
  });
5652
5681
  const agentApiServerUpdateBodySchema = passthroughObject({
5653
5682
  name: string().trim().min(1).max(100).optional(),
@@ -7321,6 +7350,19 @@ const agentApiContract = {
7321
7350
  request: { params: agentApiAttachmentDownloadParamsSchema },
7322
7351
  response: { kind: "binary" }
7323
7352
  }),
7353
+ attachmentDownloadUrl: route({
7354
+ key: "attachmentDownloadUrl",
7355
+ method: "GET",
7356
+ path: "/attachments/:attachmentId/url",
7357
+ client: {
7358
+ resource: "attachments",
7359
+ method: "downloadUrl"
7360
+ },
7361
+ capability: "read",
7362
+ description: "Mint a short-lived (5 minute) download URL for an attachment visible to the bound agent credential, with its filename and MIME type. 409 download_url_unavailable when this Server's storage cannot presign; use the binary download instead.",
7363
+ request: { params: agentApiAttachmentDownloadParamsSchema },
7364
+ response: { body: agentApiAttachmentDownloadUrlResponseSchema }
7365
+ }),
7324
7366
  attachmentCommentsList: route({
7325
7367
  key: "attachmentCommentsList",
7326
7368
  method: "GET",
@@ -8728,7 +8770,7 @@ const AGENT_API_ROUTE_MANIFEST = [
8728
8770
  "capability": "tasks",
8729
8771
  "description": "Create one or more tasks in a channel.",
8730
8772
  "sideEffect": "write",
8731
- "idempotency": "none",
8773
+ "idempotency": "key",
8732
8774
  "destructive": false,
8733
8775
  "audience": "both",
8734
8776
  "request": {
@@ -9528,7 +9570,7 @@ const AGENT_API_ROUTE_MANIFEST = [
9528
9570
  "capability": "tasks",
9529
9571
  "description": "Prepare an action card for a human to commit.",
9530
9572
  "sideEffect": "write",
9531
- "idempotency": "none",
9573
+ "idempotency": "key",
9532
9574
  "destructive": false,
9533
9575
  "audience": "both",
9534
9576
  "request": {
@@ -9716,6 +9758,31 @@ const AGENT_API_ROUTE_MANIFEST = [
9716
9758
  "body": false
9717
9759
  }
9718
9760
  },
9761
+ {
9762
+ "key": "attachmentDownloadUrl",
9763
+ "method": "GET",
9764
+ "path": "/attachments/:attachmentId/url",
9765
+ "fullPath": "/internal/agent-api/attachments/:attachmentId/url",
9766
+ "client": {
9767
+ "resource": "attachments",
9768
+ "method": "downloadUrl"
9769
+ },
9770
+ "capability": "read",
9771
+ "description": "Mint a short-lived (5 minute) download URL for an attachment visible to the bound agent credential, with its filename and MIME type. 409 download_url_unavailable when this Server's storage cannot presign; use the binary download instead.",
9772
+ "sideEffect": "read",
9773
+ "idempotency": "natural",
9774
+ "destructive": false,
9775
+ "audience": "both",
9776
+ "request": {
9777
+ "params": true,
9778
+ "query": false,
9779
+ "body": false
9780
+ },
9781
+ "response": {
9782
+ "kind": "json",
9783
+ "body": true
9784
+ }
9785
+ },
9719
9786
  {
9720
9787
  "key": "attachmentCommentsList",
9721
9788
  "method": "GET",
@@ -9843,7 +9910,7 @@ const AGENT_API_ROUTE_MANIFEST = [
9843
9910
  }
9844
9911
  ];
9845
9912
  /** Content hash of AGENT_API_ROUTE_MANIFEST; see computeAgentApiManifestVersion. */
9846
- const AGENT_API_MANIFEST_VERSION = "c7a793816a7a90ed";
9913
+ const AGENT_API_MANIFEST_VERSION = "c762a744e3542f89";
9847
9914
  //#endregion
9848
9915
  //#region src/routes.ts
9849
9916
  const ROUTE_INFO = Object.fromEntries(AGENT_API_ROUTE_MANIFEST.map((entry) => {
@@ -10438,6 +10505,280 @@ function fileReadError() {
10438
10505
  return new RaftCredentialError("CREDENTIAL_STORE_READ_FAILED", "The Raft credential file is invalid or unreadable");
10439
10506
  }
10440
10507
  //#endregion
10508
+ //#region ../shared/src/agentOps/hint.ts
10509
+ /** `tasks.updateStatus` → `tasks_update_status`: the manifest's tool name for an operation name. */
10510
+ function raftToolNameFor(name) {
10511
+ return name.replace(/\./g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
10512
+ }
10513
+ /** Tool-form wording for a CLI command that has no operation (channel / server admin writes). */
10514
+ const RAFT_NO_TOOL_WORDING = "ask a human via an action card (`actions_prepare`)";
10515
+ const ELLIPSIS = "…";
10516
+ function q(value) {
10517
+ return `"${value}"`;
10518
+ }
10519
+ function hint(cli, op, fill) {
10520
+ if (!op) return { cli };
10521
+ const args = Object.fromEntries(Object.entries(op.args).filter(([, value]) => value !== void 0));
10522
+ return fill && fill.length > 0 ? {
10523
+ cli,
10524
+ op: {
10525
+ name: op.name,
10526
+ args,
10527
+ partial: true
10528
+ },
10529
+ fill
10530
+ } : {
10531
+ cli,
10532
+ op: {
10533
+ name: op.name,
10534
+ args
10535
+ }
10536
+ };
10537
+ }
10538
+ function toolArgs(hint) {
10539
+ const parts = Object.entries(hint.op?.args ?? {}).map(([key, value]) => `${key}: ${JSON.stringify(value)}`);
10540
+ for (const key of hint.fill ?? []) parts.push(`${key}: ${ELLIPSIS}`);
10541
+ return parts.length > 0 ? `{ ${parts.join(", ")} }` : "{}";
10542
+ }
10543
+ /** Render a hint: `raft message read --target "#ops"` (cli) or `messages_read({ target: "#ops" })` (tool). */
10544
+ function formatHint(hint, style = "cli") {
10545
+ if (style === "cli") return ["raft", ...hint.cli].join(" ");
10546
+ if (!hint.op) return RAFT_NO_TOOL_WORDING;
10547
+ if (hint.codeOnly) return `raft.${hint.op.name}(${toolArgs(hint)})`;
10548
+ return `${raftToolNameFor(hint.op.name)}(${toolArgs(hint)})`;
10549
+ }
10550
+ /** Just the command's name, for prose that lists commands: `raft channel join` (cli) or `channels_join` (tool). */
10551
+ function formatHintName(hint, style = "cli") {
10552
+ if (style === "cli") return ["raft", ...hint.cli].join(" ");
10553
+ if (!hint.op) return RAFT_NO_TOOL_WORDING;
10554
+ return hint.codeOnly ? `raft.${hint.op.name}` : raftToolNameFor(hint.op.name);
10555
+ }
10556
+ /** A flag named in prose: `--limit` (cli) or `limit` (tool, the argument name). */
10557
+ function formatHintFlag(flag, arg, style = "cli") {
10558
+ return style === "cli" ? `--${flag}` : arg;
10559
+ }
10560
+ /**
10561
+ * The hint as a next step: `command` is the rendered hint, `operation` the
10562
+ * structured call (when there is one). `args` keeps the step's own arguments.
10563
+ */
10564
+ function hintStep(kind, hint, why, style = "cli", args) {
10565
+ return {
10566
+ kind,
10567
+ command: formatHint(hint, style),
10568
+ ...args ? { args } : {},
10569
+ ...hint.op ? { operation: hint.op } : {},
10570
+ why
10571
+ };
10572
+ }
10573
+ /**
10574
+ * Every hint the shared formatters and operations produce. Placeholder
10575
+ * arguments (`<name>`, `"…"`) are `fill` keys: the CLI form prints the
10576
+ * placeholder, the tool form `name: …`.
10577
+ */
10578
+ const RAFT_HINTS = {
10579
+ messageRead: ({ target, after, before, around }) => hint([
10580
+ "message",
10581
+ "read",
10582
+ "--target",
10583
+ q(target),
10584
+ ...after === void 0 ? [] : ["--after", String(after)],
10585
+ ...before === void 0 ? [] : ["--before", String(before)],
10586
+ ...around === void 0 ? [] : ["--around", around.shown]
10587
+ ], {
10588
+ name: "messages.read",
10589
+ args: {
10590
+ target,
10591
+ after,
10592
+ before,
10593
+ around: around?.id
10594
+ }
10595
+ }),
10596
+ /** Send into a conversation; `content` is always the caller's. Without a target the target is left too. */
10597
+ messageSend: ({ target, attachmentId }) => hint([
10598
+ "message",
10599
+ "send",
10600
+ ...target === void 0 ? [] : ["--target", q(target)],
10601
+ ...attachmentId === void 0 ? [] : ["--attachment-id", attachmentId]
10602
+ ], {
10603
+ name: "messages.send",
10604
+ args: {
10605
+ target,
10606
+ attachmentIds: attachmentId === void 0 ? void 0 : [attachmentId]
10607
+ }
10608
+ }, target === void 0 ? ["target", "content"] : ["content"]),
10609
+ /** The send command's name alone (prose, never a call). */
10610
+ messageSendName: () => hint(["message", "send"], {
10611
+ name: "messages.send",
10612
+ args: {}
10613
+ }, ["target", "content"]),
10614
+ messageCheck: () => hint(["message", "check"], {
10615
+ name: "inbox.check",
10616
+ args: {}
10617
+ }),
10618
+ inboxList: ({ view, before } = {}) => hint([
10619
+ "inbox",
10620
+ "check",
10621
+ ...view === void 0 ? [] : ["--view", view],
10622
+ ...before === void 0 ? [] : ["--before", String(before)]
10623
+ ], {
10624
+ name: "inbox.list",
10625
+ args: {
10626
+ view,
10627
+ before
10628
+ }
10629
+ }),
10630
+ /** `query: true` is the `<name>` placeholder. */
10631
+ serverInfo: ({ view, offset, limit, query, joined } = {}) => hint([
10632
+ "server",
10633
+ "info",
10634
+ ...view === void 0 ? [] : [`--${view}`],
10635
+ ...offset === void 0 ? [] : ["--offset", String(offset)],
10636
+ ...limit === void 0 ? [] : ["--limit", String(limit)],
10637
+ ...query === void 0 ? [] : ["--query", query === true ? "<name>" : JSON.stringify(query)],
10638
+ ...joined ? ["--joined"] : []
10639
+ ], {
10640
+ name: "server.info",
10641
+ args: {
10642
+ view,
10643
+ offset,
10644
+ limit,
10645
+ query: typeof query === "string" ? query : void 0,
10646
+ joined: joined ? true : void 0
10647
+ }
10648
+ }, query === true ? ["query"] : void 0),
10649
+ /** `name` undefined is the `<name>` placeholder. */
10650
+ userInfo: ({ name, offset, limit } = {}) => hint([
10651
+ "user",
10652
+ "info",
10653
+ name === void 0 ? "<name>" : `@${name}`,
10654
+ ...offset === void 0 ? [] : ["--offset", String(offset)],
10655
+ ...limit === void 0 ? [] : ["--limit", String(limit)]
10656
+ ], {
10657
+ name: "users.info",
10658
+ args: {
10659
+ name: name === void 0 ? void 0 : `@${name}`,
10660
+ offset,
10661
+ limit
10662
+ }
10663
+ }, name === void 0 ? ["name"] : void 0),
10664
+ /** The `<name>` placeholder form. */
10665
+ channelInfo: () => hint([
10666
+ "channel",
10667
+ "info",
10668
+ "<name>"
10669
+ ], {
10670
+ name: "channels.info",
10671
+ args: {}
10672
+ }, ["target"]),
10673
+ channelMembers: (target) => hint([
10674
+ "channel",
10675
+ "members",
10676
+ q(target)
10677
+ ], {
10678
+ name: "channels.members",
10679
+ args: { target }
10680
+ }),
10681
+ /** Channel attention commands by name (prose). */
10682
+ channelJoinName: () => hint(["channel", "join"], {
10683
+ name: "channels.join",
10684
+ args: {}
10685
+ }, ["target"]),
10686
+ channelLeaveName: () => hint(["channel", "leave"], {
10687
+ name: "channels.leave",
10688
+ args: {}
10689
+ }, ["target"]),
10690
+ channelMuteName: () => hint(["channel", "mute"], {
10691
+ name: "channels.mute",
10692
+ args: {}
10693
+ }, ["target"]),
10694
+ channelUnmuteName: () => hint(["channel", "unmute"], {
10695
+ name: "channels.unmute",
10696
+ args: {}
10697
+ }, ["target"]),
10698
+ threadUnfollowName: () => hint(["thread", "unfollow"], {
10699
+ name: "threads.unfollow",
10700
+ args: {}
10701
+ }, ["target"]),
10702
+ /** Channel / server admin writes: no operation by policy (a human acts through an action card). */
10703
+ channelCreateName: () => hint(["channel", "create"]),
10704
+ serverUpdateName: () => hint(["server", "update"]),
10705
+ taskClaim: ({ target, taskNumber }) => hint([
10706
+ "task",
10707
+ "claim",
10708
+ "--target",
10709
+ q(target),
10710
+ "--number",
10711
+ String(taskNumber)
10712
+ ], {
10713
+ name: "tasks.claim",
10714
+ args: {
10715
+ target,
10716
+ taskNumbers: [taskNumber]
10717
+ }
10718
+ }),
10719
+ taskListAll: (target) => hint([
10720
+ "task",
10721
+ "list",
10722
+ "--target",
10723
+ q(target),
10724
+ "--status",
10725
+ "all"
10726
+ ], {
10727
+ name: "tasks.list",
10728
+ args: {
10729
+ target,
10730
+ status: "all"
10731
+ }
10732
+ }),
10733
+ mentionAction: (action, resolutionId) => hint([
10734
+ "mention",
10735
+ action,
10736
+ resolutionId
10737
+ ], {
10738
+ name: "mentions.execute",
10739
+ args: {
10740
+ action,
10741
+ resolutionIds: [resolutionId]
10742
+ }
10743
+ }),
10744
+ mentionPending: () => hint(["mention", "pending"], {
10745
+ name: "mentions.pending",
10746
+ args: {}
10747
+ }),
10748
+ /** `intent` and `reason` are always the caller's. */
10749
+ manualGet: (topic) => hint([
10750
+ "manual",
10751
+ "get",
10752
+ topic,
10753
+ "--intent",
10754
+ q(ELLIPSIS),
10755
+ "--reason",
10756
+ q(ELLIPSIS)
10757
+ ], {
10758
+ name: "manual.get",
10759
+ args: { topic }
10760
+ }, ["intent", "reason"]),
10761
+ /** The download pointer on message lines; the tool form mints a URL (one attachment: its id; several: left to fill). */
10762
+ attachmentView: (attachmentId) => hint(["attachment", "view"], {
10763
+ name: "attachments.downloadUrl",
10764
+ args: { attachmentId }
10765
+ }, attachmentId === void 0 ? ["attachmentId"] : void 0),
10766
+ /** The binary download (typed method only): where an attachment whose URL cannot be minted is fetched. */
10767
+ attachmentDownload: (attachmentId) => ({
10768
+ ...hint([
10769
+ "attachment",
10770
+ "view",
10771
+ attachmentId,
10772
+ "--output",
10773
+ "<path>"
10774
+ ], {
10775
+ name: "attachments.download",
10776
+ args: { attachmentId }
10777
+ }),
10778
+ codeOnly: true
10779
+ })
10780
+ };
10781
+ //#endregion
10441
10782
  //#region ../shared/src/agentOps/outcome.ts
10442
10783
  const SERVER_ERROR_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
10443
10784
  const DEFAULT_MESSAGES = {
@@ -10449,11 +10790,14 @@ const DEFAULT_MESSAGES = {
10449
10790
  SCOPE_DENIED: "This agent's scope set does not allow this operation.",
10450
10791
  NOT_FOUND: "The target or message does not exist or is not visible to this agent.",
10451
10792
  CONFLICT: "The Raft Server refused the operation because of the current state.",
10452
- IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different message.",
10793
+ IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different request.",
10453
10794
  UNSUPPORTED_FOR_EXTERNAL_AGENTS: "This operation is not available to External Agents.",
10454
10795
  UNAVAILABLE: "The Raft Server could not serve this operation right now.",
10455
10796
  MODEL_ONLY: "This operation only counts when the model sees its result, so it cannot be run from code; nothing was sent."
10456
10797
  };
10798
+ function notFoundNextAction(style) {
10799
+ return `Check the target spelling with \`${formatHint(RAFT_HINTS.serverInfo({ view: "channels" }), style)}\` or resolve the message id first.`;
10800
+ }
10457
10801
  const DEFAULT_NEXT_ACTION = {
10458
10802
  INVALID_REQUEST: "Fix the request arguments; nothing was sent.",
10459
10803
  TRANSPORT_ERROR: "Check connectivity to the Raft Server and retry if the operation is safe to repeat.",
@@ -10461,9 +10805,9 @@ const DEFAULT_NEXT_ACTION = {
10461
10805
  INVALID_RESPONSE: "Upgrade the SDK or report the Server version; the response shape is not the one this SDK knows.",
10462
10806
  CAPABILITY_NOT_AUTHORIZED: "Ask a human who can mint credentials to include the missing capability.",
10463
10807
  SCOPE_DENIED: "Ask a human with editAgents authority to extend this agent's scopes.",
10464
- NOT_FOUND: "Check the target spelling with `raft server info --channels` or resolve the message id first.",
10808
+ NOT_FOUND: notFoundNextAction("cli"),
10465
10809
  CONFLICT: "Read the current state before repeating this operation.",
10466
- IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different message, or resend the identical payload to reconcile.",
10810
+ IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different request, or resend the identical request to reconcile.",
10467
10811
  UNSUPPORTED_FOR_EXTERNAL_AGENTS: "Use your own runtime for this; the Server does not provide it to External Agents.",
10468
10812
  UNAVAILABLE: "Retry in a moment.",
10469
10813
  MODEL_ONLY: "Call it as a model tool call instead of from code."
@@ -10552,6 +10896,49 @@ function failureOutcome(error) {
10552
10896
  text: formatOpErrorText(error)
10553
10897
  };
10554
10898
  }
10899
+ /**
10900
+ * Render the SDK's own default next action in a hint style. Only NOT_FOUND's
10901
+ * default names a command; it is set deep in the shared client-error mapping,
10902
+ * so `createRaft({ hints: "tool" })` re-renders it here instead of threading
10903
+ * the style through every failure. A Server-sent next action is left as is.
10904
+ */
10905
+ function restyleDefaultNextAction(outcome, style) {
10906
+ if (style === "cli" || !outcome || typeof outcome !== "object") return outcome;
10907
+ const failure = outcome;
10908
+ if (failure.ok !== false || !failure.error || failure.error.nextAction !== DEFAULT_NEXT_ACTION.NOT_FOUND) return outcome;
10909
+ const error = {
10910
+ ...failure.error,
10911
+ nextAction: notFoundNextAction(style)
10912
+ };
10913
+ const next = failure.next && failure.next.why === DEFAULT_NEXT_ACTION.NOT_FOUND ? {
10914
+ ...failure.next,
10915
+ why: error.nextAction
10916
+ } : failure.next ?? null;
10917
+ return {
10918
+ ...failure,
10919
+ error,
10920
+ next,
10921
+ text: formatOpErrorText(error)
10922
+ };
10923
+ }
10924
+ /**
10925
+ * A failure of a keyed write (`idempotencyKey`). A retryable failure (the
10926
+ * request may not have reached the Server, or the Server was unavailable)
10927
+ * says to repeat the SAME request with the SAME key, and carries the key in
10928
+ * `next.args.idempotencyKey`, so a caller that let the SDK generate it can
10929
+ * still retry without acting twice.
10930
+ */
10931
+ function keyedWriteFailure(failure, idempotencyKey) {
10932
+ if (!failure.error.retryable) return failure;
10933
+ return {
10934
+ ...failure,
10935
+ next: {
10936
+ kind: "retry_same_key",
10937
+ args: { idempotencyKey },
10938
+ why: "Repeat the same request with this idempotencyKey: if the first attempt was committed, the Server returns its result instead of acting twice."
10939
+ }
10940
+ };
10941
+ }
10555
10942
  /** Map a shared-client failure to a failure outcome. */
10556
10943
  function failureFromClientResult(result) {
10557
10944
  return failureOutcome(opErrorFromClientError(result.error, result.status));
@@ -10957,9 +11344,10 @@ function formatAgentSenderHandle(m) {
10957
11344
  const desc = m.sender_description ?? null;
10958
11345
  return desc ? `@${name} — ${desc}` : `@${name}`;
10959
11346
  }
10960
- function formatAgentAttachmentSuffix(attachments) {
11347
+ function formatAgentAttachmentSuffix(attachments, style = "cli") {
10961
11348
  if (!attachments?.length) return "";
10962
- return ` [${attachments.length} attachment${attachments.length > 1 ? "s" : ""}: ${attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} — use raft attachment view to download]`;
11349
+ const download = formatHint(RAFT_HINTS.attachmentView(style === "tool" && attachments.length === 1 ? attachments[0].id : void 0), style);
11350
+ return ` [${attachments.length} attachment${attachments.length > 1 ? "s" : ""}: ${attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} — use ${download} to download]`;
10963
11351
  }
10964
11352
  function formatAgentTaskAssigneeSuffix(assigneeId, assigneeName) {
10965
11353
  if (!assigneeId) return "";
@@ -10985,7 +11373,7 @@ function formatAgentTaskCurrentProjection(projection, taskNumber, neutralize) {
10985
11373
  return `\n${lines.join("\n")}`;
10986
11374
  }
10987
11375
  /** One received-message line: header bracket + sender + content + suffixes. */
10988
- function formatAgentMessageLine(m) {
11376
+ function formatAgentMessageLine(m, style = "cli") {
10989
11377
  if (m.third_party_event) {
10990
11378
  const msgId = m.message_id ? m.message_id.slice(0, 8) : m.third_party_event.id.slice(0, 8);
10991
11379
  const time = m.timestamp ? formatUtcTimestamp(m.timestamp) : "-";
@@ -11001,22 +11389,22 @@ function formatAgentMessageLine(m) {
11001
11389
  const time = m.timestamp ? formatUtcTimestamp(m.timestamp) : "-";
11002
11390
  const senderType = ` type=${m.sender_type}`;
11003
11391
  const content = indentAgentBodyContinuationLines(m.content ?? "");
11004
- const attachSuffix = formatAgentAttachmentSuffix(m.attachments);
11392
+ const attachSuffix = formatAgentAttachmentSuffix(m.attachments, style);
11005
11393
  const taskSuffix = m.task_status ? ` [task #${m.task_number} status=${m.task_status}${formatAgentTaskAssigneeSuffix(m.task_assignee_id, m.task_assignee_name)}]` : "";
11006
11394
  return `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatAgentSenderHandle(m)}: ${content}${attachSuffix}${taskSuffix}${formatAgentReplyAffordanceSuffix(m)}${formatAgentTaskCurrentProjection(m.task_current_projection)}`;
11007
11395
  }
11008
11396
  /** Batch of received-message lines (`raft message check` output). */
11009
- function formatAgentMessages(messages) {
11397
+ function formatAgentMessages(messages, style = "cli") {
11010
11398
  if (messages.length === 0) return "No new inbox messages.";
11011
- return messages.map(formatAgentMessageLine).join("\n");
11399
+ return messages.map((m) => formatAgentMessageLine(m, style)).join("\n");
11012
11400
  }
11013
11401
  /**
11014
11402
  * `message check` hands over a bounded batch, oldest first per conversation;
11015
11403
  * the server reports how many conversations still have unread after it.
11016
11404
  */
11017
- function formatAgentInboxHint(hint) {
11405
+ function formatAgentInboxHint(hint, style = "cli") {
11018
11406
  const n = hint.unread_conversations;
11019
- return `Still unread: ${n} ${n === 1 ? "conversation" : "conversations"}. Run \`raft inbox check\` to list them.`;
11407
+ return `Still unread: ${n} ${n === 1 ? "conversation" : "conversations"}. Run \`${formatHint(RAFT_HINTS.inboxList(), style)}\` to list them.`;
11020
11408
  }
11021
11409
  //#endregion
11022
11410
  //#region ../shared/src/agentOps/message.ts
@@ -11060,7 +11448,7 @@ function hasAgentMessageIdentity(envelope) {
11060
11448
  return true;
11061
11449
  }
11062
11450
  /** Project one envelope; `null` when it has no conversation identity (skip it rather than render `#undefined`). */
11063
- function projectRaftMessage(envelope) {
11451
+ function projectRaftMessage(envelope, style = "cli") {
11064
11452
  if (!hasAgentMessageIdentity(envelope)) return null;
11065
11453
  const like = toAgentMessageLike(envelope);
11066
11454
  const id = nullableString(like.message_id);
@@ -11093,7 +11481,7 @@ function projectRaftMessage(envelope) {
11093
11481
  id: nullableString(envelope.threadId),
11094
11482
  replyCount: nullableNumber(envelope.replyCount)
11095
11483
  },
11096
- text: formatAgentMessageLine(like),
11484
+ text: formatAgentMessageLine(like, style),
11097
11485
  raw: envelope
11098
11486
  };
11099
11487
  }
@@ -11101,8 +11489,8 @@ function sortBySeq(messages) {
11101
11489
  return [...messages].sort((a, b) => (a.seq ?? Number.MAX_SAFE_INTEGER) - (b.seq ?? Number.MAX_SAFE_INTEGER));
11102
11490
  }
11103
11491
  /** Project a list, skipping envelopes without a conversation identity. */
11104
- function projectRaftMessages(envelopes) {
11105
- return envelopes.map(projectRaftMessage).filter((m) => m !== null);
11492
+ function projectRaftMessages(envelopes, style = "cli") {
11493
+ return envelopes.map((envelope) => projectRaftMessage(envelope, style)).filter((m) => m !== null);
11106
11494
  }
11107
11495
  /** The conversation fields a target string names; `null` for a shape this cannot read. */
11108
11496
  function identityFromTarget(target) {
@@ -11134,12 +11522,12 @@ function identityFromTarget(target) {
11134
11522
  * because the request already named the target; fill them from `target`
11135
11523
  * instead of dropping the message.
11136
11524
  */
11137
- function projectRaftMessagesInTarget(envelopes, target) {
11525
+ function projectRaftMessagesInTarget(envelopes, target, style = "cli") {
11138
11526
  const identity = identityFromTarget(target);
11139
11527
  return envelopes.map((envelope) => projectRaftMessage(identity && !hasAgentMessageIdentity(envelope) ? {
11140
11528
  ...envelope,
11141
11529
  ...identity
11142
- } : envelope)).filter((m) => m !== null);
11530
+ } : envelope, style)).filter((m) => m !== null);
11143
11531
  }
11144
11532
  //#endregion
11145
11533
  //#region ../shared/src/agentOps/interrupt.ts
@@ -11151,7 +11539,7 @@ function isInterrupted(outcome) {
11151
11539
  function unreadMessagesInterrupt(input) {
11152
11540
  const { hold, target } = input;
11153
11541
  const withheld = input.withheld === true || hold.freshnessContextMode === "withheld";
11154
- const heldMessages = withheld ? [] : projectRaftMessagesInTarget(hold.heldMessages ?? [], target);
11542
+ const heldMessages = withheld ? [] : projectRaftMessagesInTarget(hold.heldMessages ?? [], target, input.hints);
11155
11543
  const newMessageCount = withheld ? hold.withheldMessageCount ?? hold.newMessageCount ?? 0 : hold.newMessageCount ?? heldMessages.length;
11156
11544
  const omittedMessageCount = withheld ? 0 : hold.omittedMessageCount ?? 0;
11157
11545
  return {
@@ -11442,18 +11830,9 @@ const inboxPullFields = {
11442
11830
  };
11443
11831
  const checkInboxRequestSchema = requestSchema()(object(inboxPullFields));
11444
11832
  const MAX_DRAIN_ROUNDS = 50;
11445
- function batchNext(batch) {
11446
- if (batch.hasMore) return {
11447
- kind: "check_inbox_again",
11448
- command: "raft message check",
11449
- ...batch.cursor === null ? {} : { args: { since: batch.cursor } },
11450
- why: "The Server trimmed this batch; more messages are pending."
11451
- };
11452
- if ((batch.stillUnreadConversations ?? 0) > 0) return {
11453
- kind: "list_inbox",
11454
- command: "raft inbox check",
11455
- why: "Conversations remain unread beyond this batch."
11456
- };
11833
+ function batchNext(batch, style) {
11834
+ if (batch.hasMore) return hintStep("check_inbox_again", RAFT_HINTS.messageCheck(), "The Server trimmed this batch; more messages are pending.", style, batch.cursor === null ? void 0 : { since: batch.cursor });
11835
+ if ((batch.stillUnreadConversations ?? 0) > 0) return hintStep("list_inbox", RAFT_HINTS.inboxList(), "Conversations remain unread beyond this batch.", style);
11457
11836
  if (batch.messages.length > 0) return {
11458
11837
  kind: "reply_or_act",
11459
11838
  args: { target: batch.messages[0].target },
@@ -11461,11 +11840,11 @@ function batchNext(batch) {
11461
11840
  };
11462
11841
  return null;
11463
11842
  }
11464
- function batchText(batch) {
11465
- const lines = [formatAgentMessages(batch.messages.map((m) => toAgentMessageLike(m.raw)))];
11466
- if (batch.hasMore) lines.push("More messages are pending. Run `raft message check` again.");
11843
+ function batchText(batch, style) {
11844
+ const lines = [formatAgentMessages(batch.messages.map((m) => toAgentMessageLike(m.raw)), style)];
11845
+ if (batch.hasMore) lines.push(`More messages are pending. Run \`${formatHint(RAFT_HINTS.messageCheck(), style)}\` again.`);
11467
11846
  else if (batch.messages.length > 0) lines.push("No more new inbox messages.");
11468
- if ((batch.stillUnreadConversations ?? 0) > 0) lines.push(formatAgentInboxHint({ unread_conversations: batch.stillUnreadConversations }));
11847
+ if ((batch.stillUnreadConversations ?? 0) > 0) lines.push(formatAgentInboxHint({ unread_conversations: batch.stillUnreadConversations }, style));
11469
11848
  return lines.join("\n");
11470
11849
  }
11471
11850
  function recordExactSeen(frontier, messages) {
@@ -11478,7 +11857,8 @@ function recordExactSeen(frontier, messages) {
11478
11857
  for (const [target, seqs] of byTarget) frontier.recordExact(target, seqs);
11479
11858
  }
11480
11859
  /** One bounded pull. Records exact seen seqs on `frontier` (sparse drains never advance the high-water mark). */
11481
- async function checkInbox(client, request = {}, frontier) {
11860
+ async function checkInbox(client, request = {}, frontier, options = {}) {
11861
+ const style = options.hints ?? "cli";
11482
11862
  const invalid = validateOpRequest(checkInboxRequestSchema, request);
11483
11863
  if (invalid) return invalid;
11484
11864
  const ack = request.ack ?? "cursor";
@@ -11489,7 +11869,7 @@ async function checkInbox(client, request = {}, frontier) {
11489
11869
  });
11490
11870
  if (!result.ok) return failureFromClientResult(result);
11491
11871
  const data = result.data;
11492
- const messages = sortBySeq(projectRaftMessages(data.events));
11872
+ const messages = sortBySeq(projectRaftMessages(data.events, style));
11493
11873
  recordExactSeen(frontier, messages);
11494
11874
  const batch = {
11495
11875
  messages,
@@ -11503,8 +11883,8 @@ async function checkInbox(client, request = {}, frontier) {
11503
11883
  ok: true,
11504
11884
  state: messages.length > 0 ? "batch" : "empty",
11505
11885
  data: batch,
11506
- next: batchNext(batch),
11507
- text: batchText(batch)
11886
+ next: batchNext(batch, style),
11887
+ text: batchText(batch, style)
11508
11888
  };
11509
11889
  }
11510
11890
  const drainInboxRequestSchema = requestSchema()(object(inboxPullFields));
@@ -11518,7 +11898,7 @@ const drainInboxRequestSchema = requestSchema()(object(inboxPullFields));
11518
11898
  * return value summarises the drain; the batch that ends the iteration is
11519
11899
  * acknowledged only by the caller's next pull (`summary.cursor`).
11520
11900
  */
11521
- async function* drainInbox(client, request = {}, frontier, onPull) {
11901
+ async function* drainInbox(client, request = {}, frontier, onPull, options = {}) {
11522
11902
  const ack = request.ack ?? "cursor";
11523
11903
  const invalid = validateOpRequest(drainInboxRequestSchema, request);
11524
11904
  if (invalid) return {
@@ -11540,7 +11920,7 @@ async function* drainInbox(client, request = {}, frontier, onPull) {
11540
11920
  ...sent === null ? {} : { since: sent },
11541
11921
  limit: request.limit,
11542
11922
  ack
11543
- }, frontier);
11923
+ }, frontier, options);
11544
11924
  if (!round.ok) return {
11545
11925
  cursor,
11546
11926
  ackMode,
@@ -11590,25 +11970,18 @@ async function* drainInbox(client, request = {}, frontier, onPull) {
11590
11970
  };
11591
11971
  }
11592
11972
  /** Fold a finished drain into one outcome; a drain that failed before its first batch is that failure. */
11593
- function drainedInboxOutcome(batches, summary) {
11973
+ function drainedInboxOutcome(batches, summary, options = {}) {
11594
11974
  if (summary.error && batches.length === 0) return summary.error;
11975
+ const style = options.hints ?? "cli";
11595
11976
  const messages = batches.flatMap((batch) => batch.messages);
11596
- const lines = [formatAgentMessages(messages.map((m) => toAgentMessageLike(m.raw)))];
11977
+ const lines = [formatAgentMessages(messages.map((m) => toAgentMessageLike(m.raw)), style)];
11597
11978
  if (summary.error) lines.push("The drain stopped early on an error; more messages may be pending. Check the inbox again.");
11598
11979
  else if (summary.hasMore) lines.push("More messages are pending. Check the inbox again.");
11599
11980
  else if (messages.length > 0) lines.push("No more new inbox messages.");
11600
11981
  const stillUnread = summary.stillUnreadConversations ?? 0;
11601
- if (stillUnread > 0) lines.push(formatAgentInboxHint({ unread_conversations: stillUnread }));
11982
+ if (stillUnread > 0) lines.push(formatAgentInboxHint({ unread_conversations: stillUnread }, style));
11602
11983
  const first = messages[0];
11603
- const next = summary.hasMore ? {
11604
- kind: "check_inbox_again",
11605
- command: "raft message check",
11606
- why: "More messages are pending."
11607
- } : stillUnread > 0 ? {
11608
- kind: "list_inbox",
11609
- command: "raft inbox check",
11610
- why: "Conversations remain unread beyond this drain."
11611
- } : first ? {
11984
+ const next = summary.hasMore ? hintStep("check_inbox_again", RAFT_HINTS.messageCheck(), "More messages are pending.", style) : stillUnread > 0 ? hintStep("list_inbox", RAFT_HINTS.inboxList(), "Conversations remain unread beyond this drain.", style) : first ? {
11612
11985
  kind: "reply_or_act",
11613
11986
  args: { target: first.target },
11614
11987
  why: "Handle the messages above; reply where each one came from."
@@ -11630,23 +12003,21 @@ const listInboxRequestSchema = requestSchema()(object({
11630
12003
  before: number$1().int().nonnegative().optional().describe("Next page: the nextBeforeSeq of the previous page."),
11631
12004
  limit: number$1().int().positive().optional().describe("Conversations per page, 1..50 (Server default 20).")
11632
12005
  }));
11633
- function openCommandFor(item) {
11634
- return `raft message read --target "${item.target}" --after ${item.lastReadSeq}`;
12006
+ function openHint(item) {
12007
+ return RAFT_HINTS.messageRead({
12008
+ target: item.target,
12009
+ after: item.lastReadSeq
12010
+ });
11635
12011
  }
11636
- function listingNext(listing) {
12012
+ function listingNext(listing, style) {
11637
12013
  const first = listing.conversations[0];
11638
- if (first) return {
11639
- kind: "read_target",
11640
- command: first.openCommand,
11641
- args: {
11642
- target: first.target,
11643
- after: first.lastReadSeq
11644
- },
11645
- why: "Open the newest unread conversation from your read position."
11646
- };
12014
+ if (first) return hintStep("read_target", openHint(first), "Open the newest unread conversation from your read position.", style, {
12015
+ target: first.target,
12016
+ after: first.lastReadSeq
12017
+ });
11647
12018
  return null;
11648
12019
  }
11649
- function listingText(listing) {
12020
+ function listingText(listing, style) {
11650
12021
  const t = listing.totals;
11651
12022
  const head = t.conversations === 0 ? "Inbox: nothing unread." : `Inbox: ${t.conversations} unread ${t.conversations === 1 ? "conversation" : "conversations"} (${t.dms} DMs, ${t.mentions} with mentions).${listing.conversations.length > 0 ? " Newest activity first." : ""}`;
11652
12023
  const rows = listing.conversations.map((c) => {
@@ -11654,8 +12025,14 @@ function listingText(listing) {
11654
12025
  return `${c.target} · ${c.unread} unread${flags ? ` · ${flags}` : ""}${c.latestSenderName ? ` · latest @${c.latestSenderName}` : ""}\n open: ${c.openCommand}`;
11655
12026
  });
11656
12027
  const trailer = [];
11657
- if (listing.hasMore && listing.nextBeforeSeq !== null) trailer.push(`More: raft inbox check${listing.view === "mentions" ? " --view mentions" : ""} --before ${listing.nextBeforeSeq}`);
11658
- const next = listingNext(listing);
12028
+ if (listing.hasMore && listing.nextBeforeSeq !== null) {
12029
+ const more = RAFT_HINTS.inboxList({
12030
+ ...listing.view === "mentions" ? { view: "mentions" } : {},
12031
+ before: listing.nextBeforeSeq
12032
+ });
12033
+ trailer.push(`More: ${formatHint(more, style)}`);
12034
+ }
12035
+ const next = listingNext(listing, style);
11659
12036
  trailer.push(next?.command ? `Next: open the first conversation above: ${next.command}` : "Next: nothing to do.");
11660
12037
  return [
11661
12038
  head,
@@ -11664,7 +12041,8 @@ function listingText(listing) {
11664
12041
  ].join("\n");
11665
12042
  }
11666
12043
  /** The agent's Activity panel. Lists only; nothing is consumed. */
11667
- async function listInbox(client, request = {}) {
12044
+ async function listInbox(client, request = {}, options = {}) {
12045
+ const style = options.hints ?? "cli";
11668
12046
  const invalid = validateOpRequest(listInboxRequestSchema, request);
11669
12047
  if (invalid) return invalid;
11670
12048
  const result = await client.inbox.list({
@@ -11685,7 +12063,7 @@ async function listInbox(client, request = {}) {
11685
12063
  activitySeq: item.activitySeq,
11686
12064
  latestSenderName: item.latestSenderName,
11687
12065
  latestAt: item.latestAt,
11688
- openCommand: openCommandFor(item)
12066
+ openCommand: formatHint(openHint(item), style)
11689
12067
  })),
11690
12068
  totals: data.totals,
11691
12069
  hasMore: data.hasMore,
@@ -11695,8 +12073,8 @@ async function listInbox(client, request = {}) {
11695
12073
  ok: true,
11696
12074
  state: listing.conversations.length > 0 ? "listed" : "empty",
11697
12075
  data: listing,
11698
- next: listingNext(listing),
11699
- text: listingText(listing)
12076
+ next: listingNext(listing, style),
12077
+ text: listingText(listing, style)
11700
12078
  };
11701
12079
  }
11702
12080
  //#endregion
@@ -11711,38 +12089,34 @@ const readHistoryRequestSchema = requestSchema()(object({
11711
12089
  limit: number$1().int().positive().optional().describe("Maximum messages in the window."),
11712
12090
  consume: boolean().optional().describe("false reads without consuming: the conversation is not marked read and nothing counts as seen.")
11713
12091
  }));
11714
- function historyNext(page) {
12092
+ function historyNext(page, style) {
11715
12093
  const seqs = page.messages.map((m) => m.seq).filter((s) => s !== null);
11716
12094
  if (page.hasNewer && seqs.length > 0) {
11717
12095
  const max = Math.max(...seqs);
11718
- return {
11719
- kind: "read_newer",
11720
- command: `raft message read --target "${page.target}" --after ${max}`,
11721
- args: {
11722
- target: page.target,
11723
- after: max
11724
- },
11725
- why: "Newer messages exist beyond this window."
11726
- };
12096
+ return hintStep("read_newer", RAFT_HINTS.messageRead({
12097
+ target: page.target,
12098
+ after: max
12099
+ }), "Newer messages exist beyond this window.", style, {
12100
+ target: page.target,
12101
+ after: max
12102
+ });
11727
12103
  }
11728
12104
  if (page.hasOlder && seqs.length > 0) {
11729
12105
  const min = Math.min(...seqs);
11730
- return {
11731
- kind: "read_older",
11732
- command: `raft message read --target "${page.target}" --before ${min}`,
11733
- args: {
11734
- target: page.target,
11735
- before: min
11736
- },
11737
- why: "Older messages exist before this window; read them only if the task needs them."
11738
- };
12106
+ return hintStep("read_older", RAFT_HINTS.messageRead({
12107
+ target: page.target,
12108
+ before: min
12109
+ }), "Older messages exist before this window; read them only if the task needs them.", style, {
12110
+ target: page.target,
12111
+ before: min
12112
+ });
11739
12113
  }
11740
12114
  return null;
11741
12115
  }
11742
- function historyText(page) {
12116
+ function historyText(page, style) {
11743
12117
  if (page.messages.length === 0) return `No messages in ${page.target}.`;
11744
- const lines = [formatAgentMessages(page.messages.map((m) => toAgentMessageLike(m.raw)))];
11745
- const next = historyNext(page);
12118
+ const lines = [formatAgentMessages(page.messages.map((m) => toAgentMessageLike(m.raw)), style)];
12119
+ const next = historyNext(page, style);
11746
12120
  if (next?.command) lines.push(`${next.kind === "read_newer" ? "Newer" : "Older"} exist: ${next.command}`);
11747
12121
  return lines.join("\n");
11748
12122
  }
@@ -11762,7 +12136,8 @@ function recordHistorySeen(frontier, requested, page, around) {
11762
12136
  }
11763
12137
  frontier.recordExact(page.target, seqs);
11764
12138
  }
11765
- async function readHistory(client, request, frontier) {
12139
+ async function readHistory(client, request, frontier, options = {}) {
12140
+ const style = options.hints ?? "cli";
11766
12141
  const invalid = validateOpRequest(readHistoryRequestSchema, request);
11767
12142
  if (invalid) return invalid;
11768
12143
  if (!request.target?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A target is required to read history." }));
@@ -11778,7 +12153,7 @@ async function readHistory(client, request, frontier) {
11778
12153
  const data = result.data;
11779
12154
  const page = {
11780
12155
  target: typeof data.target === "string" && data.target ? data.target : request.target,
11781
- messages: sortBySeq(projectRaftMessages(data.messages)),
12156
+ messages: sortBySeq(projectRaftMessages(data.messages, style)),
11782
12157
  hasOlder: data.has_older === true,
11783
12158
  hasNewer: data.has_newer === true,
11784
12159
  lastReadSeq: typeof data.last_read_seq === "number" ? data.last_read_seq : null,
@@ -11789,8 +12164,8 @@ async function readHistory(client, request, frontier) {
11789
12164
  ok: true,
11790
12165
  state: page.messages.length > 0 ? "page" : "empty",
11791
12166
  data: page,
11792
- next: historyNext(page),
11793
- text: historyText(page)
12167
+ next: historyNext(page, style),
12168
+ text: historyText(page, style)
11794
12169
  };
11795
12170
  }
11796
12171
  /** Attempts for a keyed send whose request never reached the Server (transport failure only). */
@@ -11814,68 +12189,60 @@ const replyToRequestSchema = requestSchema()(object({
11814
12189
  ...sendMessageFields
11815
12190
  }));
11816
12191
  /** Today's held text (unchanged); it is the interrupt's `context` and the outcome's `text`. */
11817
- function heldText(held, action) {
12192
+ function heldText(held, action, style) {
11818
12193
  const noun = held.newMessageCount === 1 ? "message" : "messages";
11819
12194
  const head = `Held — ${held.newMessageCount} unread ${noun} in ${held.target}. ${action}`;
11820
12195
  if (held.withheld) return `${head}\nContext withheld (reviewer isolation).`;
11821
12196
  const lines = [head];
11822
12197
  if (held.formalMentionCount > 0) lines.push(`Note: ${held.formalMentionCount} of these messages formally @mention you.`);
11823
12198
  if (held.omittedMessageCount > 0) lines.push(`${held.omittedMessageCount} earlier ${held.omittedMessageCount === 1 ? "message" : "messages"} not shown.`);
11824
- if (held.heldMessages.length > 0) lines.push(formatAgentMessages(held.heldMessages.map((m) => toAgentMessageLike(m.raw))));
12199
+ if (held.heldMessages.length > 0) lines.push(formatAgentMessages(held.heldMessages.map((m) => toAgentMessageLike(m.raw)), style));
11825
12200
  if (!held.contextComplete) lines.push("Not all of them are shown here; read the conversation before sending again.");
11826
- lines.push(`Full text: raft message read --target "${held.target}"`);
12201
+ lines.push(`Full text: ${formatHint(RAFT_HINTS.messageRead({ target: held.target }), style)}`);
11827
12202
  return lines.join("\n");
11828
12203
  }
11829
12204
  /**
11830
12205
  * A Server freshness hold as the `unread_messages` interrupt: the structured
11831
12206
  * details, today's held text as `context`, and the given resume / cancel.
11832
12207
  */
11833
- function heldInterrupt(target, data, action, resume, cancel) {
12208
+ function heldInterrupt(target, data, action, resume, cancel, style = "cli") {
11834
12209
  const interrupt = unreadMessagesInterrupt({
11835
12210
  target,
11836
12211
  hold: data,
11837
12212
  context: "",
11838
12213
  resume,
11839
- ...cancel ? { cancel } : {}
12214
+ ...cancel ? { cancel } : {},
12215
+ hints: style
11840
12216
  });
11841
12217
  return {
11842
12218
  ...interrupt,
11843
- context: heldText(interrupt, action)
12219
+ context: heldText(interrupt, action, style)
11844
12220
  };
11845
12221
  }
11846
- function heldSendNext(interrupt) {
11847
- return {
11848
- kind: "resend",
11849
- command: `raft message read --target "${interrupt.target}"`,
11850
- args: { target: interrupt.target },
11851
- why: interrupt.withheld ? "Newer messages exist in this conversation but were withheld; read them, then resume the send (interrupt.resume, same idempotencyKey) or cancel it." : !interrupt.contextComplete ? "Newer messages arrived in this conversation and not all of them are shown here; read the conversation, then resume the send (interrupt.resume, same idempotencyKey) or cancel it." : "Newer messages arrived in this conversation. Show interrupt.context to the model, call frontier.recordHeld(interrupt) to attest that, then resume the send (interrupt.resume, same idempotencyKey) or cancel it."
11852
- };
12222
+ function heldSendNext(interrupt, style) {
12223
+ return hintStep("resend", RAFT_HINTS.messageRead({ target: interrupt.target }), interrupt.withheld ? "Newer messages exist in this conversation but were withheld; read them, then resume the send (interrupt.resume, same idempotencyKey) or cancel it." : !interrupt.contextComplete ? "Newer messages arrived in this conversation and not all of them are shown here; read the conversation, then resume the send (interrupt.resume, same idempotencyKey) or cancel it." : "Newer messages arrived in this conversation. Show interrupt.context to the model, call frontier.recordHeld(interrupt) to attest that, then resume the send (interrupt.resume, same idempotencyKey) or cancel it.", style, { target: interrupt.target });
11853
12224
  }
11854
12225
  function isHeldResponse(data) {
11855
12226
  return Boolean(data) && typeof data === "object" && data.state === "held";
11856
12227
  }
11857
- function sentOutcome(target, data) {
12228
+ function sentOutcome(target, data, style) {
11858
12229
  const sent = {
11859
12230
  messageId: data.messageId,
11860
12231
  messageSeq: typeof data.messageSeq === "number" ? data.messageSeq : null,
11861
12232
  unresolvedMentionHandles: data.unresolvedMentionHandles ?? [],
11862
- recentUnread: projectRaftMessages(data.recentUnread ?? [])
12233
+ recentUnread: projectRaftMessages(data.recentUnread ?? [], style)
11863
12234
  };
11864
12235
  const warn = sent.unresolvedMentionHandles.length > 0 ? ` Unresolved @handles: ${sent.unresolvedMentionHandles.map((h) => `@${h}`).join(", ")}.` : "";
11865
12236
  return {
11866
12237
  ok: true,
11867
12238
  state: "sent",
11868
12239
  data: sent,
11869
- next: sent.recentUnread.length > 0 ? {
11870
- kind: "read_target",
11871
- command: `raft message read --target "${target}"`,
11872
- args: { target },
11873
- why: "Newer messages arrived while you were sending."
11874
- } : null,
12240
+ next: sent.recentUnread.length > 0 ? hintStep("read_target", RAFT_HINTS.messageRead({ target }), "Newer messages arrived while you were sending.", style, { target }) : null,
11875
12241
  text: `Message sent to ${target}. Message ID: ${sent.messageId}${warn}`
11876
12242
  };
11877
12243
  }
11878
- async function sendMessage(client, request, frontier) {
12244
+ async function sendMessage(client, request, frontier, options = {}) {
12245
+ const style = options.hints ?? "cli";
11879
12246
  const invalid = validateOpRequest(sendMessageRequestSchema, request);
11880
12247
  if (invalid) return invalid;
11881
12248
  if (!request.target?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A target is required to send a message." }));
@@ -11899,16 +12266,16 @@ async function sendMessage(client, request, frontier) {
11899
12266
  if (!result.ok) return failureFromClientResult(result);
11900
12267
  const data = result.data;
11901
12268
  if (isHeldResponse(data)) {
11902
- const interrupt = heldInterrupt(request.target, data, "Your message was not sent.", inProcessSendResume(idempotencyKey));
12269
+ const interrupt = heldInterrupt(request.target, data, "Your message was not sent.", inProcessSendResume(idempotencyKey), void 0, style);
11903
12270
  return {
11904
12271
  ok: true,
11905
12272
  state: "interrupted",
11906
12273
  interrupt,
11907
- next: heldSendNext(interrupt),
12274
+ next: heldSendNext(interrupt, style),
11908
12275
  text: interrupt.context
11909
12276
  };
11910
12277
  }
11911
- if (data.state === "sent") return sentOutcome(request.target, data);
12278
+ if (data.state === "sent") return sentOutcome(request.target, data, style);
11912
12279
  return failureOutcome(opError("INVALID_RESPONSE", { message: `Unexpected send state "${data.state}".` }));
11913
12280
  }
11914
12281
  function taskTimestamps(t) {
@@ -11990,13 +12357,13 @@ function formatAgentMyTaskList(data, statusFilter) {
11990
12357
  ].join("\n");
11991
12358
  }
11992
12359
  /** Receipt for task create. */
11993
- function formatAgentTasksCreated(channel, data) {
12360
+ function formatAgentTasksCreated(channel, data, style = "cli") {
11994
12361
  const created = data.tasks.map((t) => {
11995
12362
  const assignee = t.claimedById ? t.claimedByName ? `@${t.claimedByName}` : "<unresolved>" : "unassigned";
11996
12363
  const resourceReceipt = t.requiresResourceReceipt ? " resource-receipt=pending" : "";
11997
12364
  return `#${t.taskNumber} [${t.status}] assignee=${assignee} claimedAt=${t.claimedAt ?? "null"} msg=${t.messageId.slice(0, 8)}${resourceReceipt} "${t.title}"`;
11998
12365
  }).join("\n");
11999
- const threadHints = data.tasks.map((t) => `#${t.taskNumber} → raft message send --target "${channel}:${t.messageId.slice(0, 8)}"`).join("\n");
12366
+ const threadHints = data.tasks.map((t) => `#${t.taskNumber} → ${formatHint(RAFT_HINTS.messageSend({ target: `${channel}:${t.messageId.slice(0, 8)}` }), style)}`).join("\n");
12000
12367
  const receipt = data.assignmentReceipt ? `\n\nAssignment receipt (msg=${data.assignmentReceipt.messageId.slice(0, 8)}):\n${data.assignmentReceipt.content}` : "";
12001
12368
  return `Created ${data.tasks.length} task(s) in ${channel}:\n${created}${receipt}\n\nTo follow up in each task's thread:\n${threadHints}`;
12002
12369
  }
@@ -12019,7 +12386,7 @@ function agentTaskThreadTarget(target, messageId) {
12019
12386
  return `${canonicalAgentTaskTarget(target)}:${messageId.slice(0, 8)}`;
12020
12387
  }
12021
12388
  /** Claim results incl. concurrency-lock guidance on failed claims. */
12022
- function formatAgentClaimResults(channel, data) {
12389
+ function formatAgentClaimResults(channel, data, style = "cli") {
12023
12390
  const lines = data.results.map((r) => {
12024
12391
  const label = r.taskNumber ? `#${r.taskNumber}` : `msg:${r.messageId}`;
12025
12392
  if (r.success) return `${label} (msg:${r.messageId ? r.messageId.slice(0, 8) : ""}): claimed`;
@@ -12031,7 +12398,7 @@ function formatAgentClaimResults(channel, data) {
12031
12398
  const failed = data.results.length - succeeded;
12032
12399
  let summary = `${succeeded} claimed`;
12033
12400
  if (failed > 0) summary += `, ${failed} failed`;
12034
- const claimedMsgs = data.results.filter((r) => r.success && r.messageId).map((r) => `#${r.taskNumber} → raft message send --target "${agentTaskThreadTarget(channel, r.messageId)}"`).join("\n");
12401
+ const claimedMsgs = data.results.filter((r) => r.success && r.messageId).map((r) => `#${r.taskNumber} → ${formatHint(RAFT_HINTS.messageSend({ target: agentTaskThreadTarget(channel, r.messageId) }), style)}`).join("\n");
12035
12402
  const threadHint = claimedMsgs ? `\n\nFollow up in each task's thread:\n${claimedMsgs}` : "";
12036
12403
  return `Claim results (${summary}):\n${lines.join("\n")}${threadHint}`;
12037
12404
  }
@@ -12048,13 +12415,13 @@ function formatAgentTaskDeleted(taskNumber) {
12048
12415
  return `#${taskNumber} deleted.`;
12049
12416
  }
12050
12417
  /** Message→task conversion receipt. */
12051
- function formatAgentTaskConverted(channel, task) {
12418
+ function formatAgentTaskConverted(channel, task, style = "cli") {
12052
12419
  const target = `${canonicalAgentTaskTarget(channel)}:${task.messageId.slice(0, 8)}`;
12053
12420
  return [
12054
12421
  `Converted msg=${task.messageId.slice(0, 8)} to task #${task.taskNumber} [${task.status}] assignee=unassigned "${task.title}"`,
12055
12422
  "",
12056
12423
  `To follow up in the task's thread:`,
12057
- `raft message send --target "${target}"`
12424
+ formatHint(RAFT_HINTS.messageSend({ target }), style)
12058
12425
  ].join("\n");
12059
12426
  }
12060
12427
  function formatAgentTaskAmended(data) {
@@ -12116,15 +12483,16 @@ const claimTasksRequestSchema = requestSchema()(object({
12116
12483
  * A held task call: the interrupt whose resume is the identical command, with
12117
12484
  * no cancel (a held task call saved nothing). `context` is today's held text.
12118
12485
  */
12119
- function heldTaskInterrupt(target, data, action, after, argv) {
12486
+ function heldTaskInterrupt(target, data, action, after, argv, style) {
12120
12487
  const interrupt = unreadMessagesInterrupt({
12121
12488
  target,
12122
12489
  hold: data,
12123
12490
  context: "",
12124
- resume: { argv }
12491
+ resume: { argv },
12492
+ hints: style
12125
12493
  });
12126
12494
  const noun = interrupt.newMessageCount === 1 ? "message" : "messages";
12127
- const context = `Held — ${interrupt.newMessageCount} unread ${noun} in ${target}. ${action}\nRead them with: raft message read --target "${target}"\n${after}`;
12495
+ const context = `Held — ${interrupt.newMessageCount} unread ${noun} in ${target}. ${action}\nRead them with: ${formatHint(RAFT_HINTS.messageRead({ target }), style)}\n${after}`;
12128
12496
  return {
12129
12497
  ...interrupt,
12130
12498
  context
@@ -12136,17 +12504,14 @@ function rowState(result) {
12136
12504
  if (result.conflict?.kind === "claim_conflict") return "conflict";
12137
12505
  return "refused";
12138
12506
  }
12139
- function claimNext(claim) {
12507
+ function claimNext(claim, style) {
12140
12508
  const first = claim.rows.find((row) => row.mayWork);
12141
12509
  if (first) {
12142
12510
  const thread = first.messageId ? agentTaskThreadTarget(claim.target, first.messageId) : null;
12143
- return {
12511
+ const why = "The claim is yours; post progress in the task's thread.";
12512
+ return thread ? hintStep("start_work", RAFT_HINTS.messageSend({ target: thread }), why, style, { target: thread }) : {
12144
12513
  kind: "start_work",
12145
- ...thread ? {
12146
- command: `raft message send --target "${thread}"`,
12147
- args: { thread }
12148
- } : {},
12149
- why: "The claim is yours; post progress in the task's thread."
12514
+ why
12150
12515
  };
12151
12516
  }
12152
12517
  return {
@@ -12154,10 +12519,11 @@ function claimNext(claim) {
12154
12519
  why: "No claim authorised work. Do not retry the identical claim and do not start conflicting execution; if you own this lane, correct the routing in the original thread."
12155
12520
  };
12156
12521
  }
12157
- function claimText(claim) {
12158
- return formatAgentClaimResults(claim.target, { results: claim.rows.map((row) => row.raw) });
12522
+ function claimText(claim, style) {
12523
+ return formatAgentClaimResults(claim.target, { results: claim.rows.map((row) => row.raw) }, style);
12159
12524
  }
12160
- async function claimTasks(client, request) {
12525
+ async function claimTasks(client, request, options = {}) {
12526
+ const style = options.hints ?? "cli";
12161
12527
  const numbers = (request.taskNumbers ?? []).filter((n) => Number.isInteger(n) && n > 0);
12162
12528
  const ids = (request.messageIds ?? []).map((id) => id.trim()).filter(Boolean);
12163
12529
  const invalid = validateOpRequest(claimTasksRequestSchema, request);
@@ -12172,17 +12538,12 @@ async function claimTasks(client, request) {
12172
12538
  if (!result.ok) return failureFromClientResult(result);
12173
12539
  const data = result.data;
12174
12540
  if (isHeldResponse(data)) {
12175
- const interrupt = heldTaskInterrupt(request.target, data, "Your task claim was not applied.", "After reviewing, rerun the claim if it is still correct.", taskClaimArgv(request.target, numbers, ids));
12541
+ const interrupt = heldTaskInterrupt(request.target, data, "Your task claim was not applied.", "After reviewing, rerun the claim if it is still correct.", taskClaimArgv(request.target, numbers, ids), style);
12176
12542
  return {
12177
12543
  ok: true,
12178
12544
  state: "interrupted",
12179
12545
  interrupt,
12180
- next: {
12181
- kind: "retry_claim",
12182
- command: `raft message read --target "${request.target}"`,
12183
- args: { target: request.target },
12184
- why: "Unread messages in this channel may change the task; read them (frontier.recordHeld(interrupt) once the model saw them), then resume the claim (interrupt.resume) if it is still right."
12185
- },
12546
+ next: hintStep("retry_claim", RAFT_HINTS.messageRead({ target: request.target }), "Unread messages in this channel may change the task; read them (frontier.recordHeld(interrupt) once the model saw them), then resume the claim (interrupt.resume) if it is still right.", style, { target: request.target }),
12186
12547
  text: interrupt.context
12187
12548
  };
12188
12549
  }
@@ -12209,8 +12570,8 @@ async function claimTasks(client, request) {
12209
12570
  ok: true,
12210
12571
  state: !claim.anyAuthorised ? "refused" : rows.every((row) => row.mayWork) ? "claimed" : "partial",
12211
12572
  data: claim,
12212
- next: claimNext(claim),
12213
- text: claimText(claim)
12573
+ next: claimNext(claim, style),
12574
+ text: claimText(claim, style)
12214
12575
  };
12215
12576
  }
12216
12577
  const listTasksRequestSchema = requestSchema()(object({
@@ -12225,7 +12586,7 @@ const listTasksRequestSchema = requestSchema()(object({
12225
12586
  "all"
12226
12587
  ]).optional().describe("Filter by status; all includes done and closed.")
12227
12588
  }));
12228
- async function listTasks(client, request) {
12589
+ async function listTasks(client, request, options = {}) {
12229
12590
  const invalid = validateOpRequest(listTasksRequestSchema, request);
12230
12591
  if (invalid) return invalid;
12231
12592
  const mine = request.mine === true;
@@ -12244,15 +12605,13 @@ async function listTasks(client, request) {
12244
12605
  };
12245
12606
  const text = mine ? formatAgentMyTaskList(data, request.status) : formatAgentTaskList(request.target, data, request.status);
12246
12607
  const open = board.tasks.find((t) => t.status === "todo" && !t.claimedByName);
12247
- const next = open?.taskNumber && board.target ? {
12248
- kind: "claim_task",
12249
- command: `raft task claim --target "${board.target}" --number ${open.taskNumber}`,
12250
- args: {
12251
- target: board.target,
12252
- taskNumbers: [open.taskNumber]
12253
- },
12254
- why: "An unassigned todo task is open; claim it before working on it."
12255
- } : null;
12608
+ const next = open?.taskNumber && board.target ? hintStep("claim_task", RAFT_HINTS.taskClaim({
12609
+ target: board.target,
12610
+ taskNumber: open.taskNumber
12611
+ }), "An unassigned todo task is open; claim it before working on it.", options.hints, {
12612
+ target: board.target,
12613
+ taskNumbers: [open.taskNumber]
12614
+ }) : null;
12256
12615
  return {
12257
12616
  ok: true,
12258
12617
  state: board.tasks.length > 0 ? "board" : "empty",
@@ -12267,21 +12626,24 @@ const createTasksRequestSchema = requestSchema()(object({
12267
12626
  title: string().describe("Task title."),
12268
12627
  createsResource: boolean().optional().describe("The task produces a resource (for example a document) that needs a receipt.")
12269
12628
  })).describe("One entry per task to create."),
12270
- assignee: string().optional().describe("`@handle`: yourself to start in_progress, or (owner/admin) someone else to reserve a todo.")
12629
+ assignee: string().optional().describe("`@handle`: yourself to start in_progress, or (owner/admin) someone else to reserve a todo."),
12630
+ idempotencyKey: string().optional().describe("One key per logical create; generated when omitted and returned. Repeat the same request with the same key within 24 hours to retry without creating the tasks twice.")
12271
12631
  }));
12272
- async function createTasks(client, request) {
12632
+ async function createTasks(client, request, options = {}) {
12273
12633
  const invalid = validateOpRequest(createTasksRequestSchema, request);
12274
12634
  if (invalid) return invalid;
12275
12635
  if (!request.target?.trim() || !request.tasks?.length) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel target and at least one task title are required." }));
12636
+ const idempotencyKey = request.idempotencyKey?.trim() || globalThis.crypto.randomUUID();
12276
12637
  const result = await client.tasks.create({
12277
12638
  channel: request.target,
12278
12639
  tasks: request.tasks.map((t) => ({
12279
12640
  title: t.title,
12280
12641
  ...t.createsResource ? { creates_resource: true } : {}
12281
12642
  })),
12282
- ...request.assignee ? { assignee: request.assignee } : {}
12643
+ ...request.assignee ? { assignee: request.assignee } : {},
12644
+ idempotencyKey
12283
12645
  });
12284
- if (!result.ok) return failureFromClientResult(result);
12646
+ if (!result.ok) return keyedWriteFailure(failureFromClientResult(result), idempotencyKey);
12285
12647
  const data = result.data;
12286
12648
  const first = data.tasks[0];
12287
12649
  return {
@@ -12289,17 +12651,18 @@ async function createTasks(client, request) {
12289
12651
  state: "created",
12290
12652
  data: {
12291
12653
  ...data,
12292
- target: request.target
12654
+ target: request.target,
12655
+ idempotencyKey
12293
12656
  },
12294
- next: first ? {
12295
- kind: "post_in_task_thread",
12296
- command: `raft message send --target "${agentTaskThreadTarget(request.target, first.messageId)}"`,
12297
- args: { thread: agentTaskThreadTarget(request.target, first.messageId) },
12298
- why: "Follow up in each task's thread."
12299
- } : null,
12300
- text: formatAgentTasksCreated(request.target, data)
12657
+ next: first ? taskThreadStep(request.target, first.messageId, "Follow up in each task's thread.", options.hints) : null,
12658
+ text: formatAgentTasksCreated(request.target, data, options.hints)
12301
12659
  };
12302
12660
  }
12661
+ /** Post in a task's thread (a send: `content` is the caller's). */
12662
+ function taskThreadStep(target, messageId, why, style = "cli") {
12663
+ const thread = agentTaskThreadTarget(target, messageId);
12664
+ return hintStep("post_in_task_thread", RAFT_HINTS.messageSend({ target: thread }), why, style, { target: thread });
12665
+ }
12303
12666
  const taskRefFields = {
12304
12667
  target: taskChannelSchema,
12305
12668
  taskNumber: taskNumberSchema
@@ -12389,22 +12752,17 @@ const updateTaskStatusRequestSchema = requestSchema()(object({
12389
12752
  status: raftTaskStatusSchema.describe("todo → in_progress → in_review → done; closed from anywhere.")
12390
12753
  }));
12391
12754
  /** A held task write: the interrupt whose resume is the identical command; nothing to cancel. */
12392
- function heldTaskWrite(target, data, action, kind, argv) {
12393
- const interrupt = heldTaskInterrupt(target, data, action, "After reviewing, repeat the operation if it is still correct.", argv);
12755
+ function heldTaskWrite(target, data, action, kind, argv, style) {
12756
+ const interrupt = heldTaskInterrupt(target, data, action, "After reviewing, repeat the operation if it is still correct.", argv, style);
12394
12757
  return {
12395
12758
  ok: true,
12396
12759
  state: "interrupted",
12397
12760
  interrupt,
12398
- next: {
12399
- kind,
12400
- command: `raft message read --target "${target}"`,
12401
- args: { target },
12402
- why: "Unread messages in this channel may change the task; read them (frontier.recordHeld(interrupt) once the model saw them), then resume (interrupt.resume) if it is still right."
12403
- },
12761
+ next: hintStep(kind, RAFT_HINTS.messageRead({ target }), "Unread messages in this channel may change the task; read them (frontier.recordHeld(interrupt) once the model saw them), then resume (interrupt.resume) if it is still right.", style, { target }),
12404
12762
  text: interrupt.context
12405
12763
  };
12406
12764
  }
12407
- async function updateTaskStatus(client, request) {
12765
+ async function updateTaskStatus(client, request, options = {}) {
12408
12766
  const invalid = requireTaskRef(request, updateTaskStatusRequestSchema);
12409
12767
  if (invalid) return invalid;
12410
12768
  const result = await client.tasks.updateStatus({
@@ -12413,7 +12771,7 @@ async function updateTaskStatus(client, request) {
12413
12771
  status: request.status
12414
12772
  });
12415
12773
  if (!result.ok) return failureFromClientResult(result);
12416
- if (isHeldResponse(result.data)) return heldTaskWrite(request.target, result.data, "The status change was not applied.", "retry_update_status", taskUpdateArgv(request.target, request.taskNumber, request.status));
12774
+ if (isHeldResponse(result.data)) return heldTaskWrite(request.target, result.data, "The status change was not applied.", "retry_update_status", taskUpdateArgv(request.target, request.taskNumber, request.status), options.hints ?? "cli");
12417
12775
  return {
12418
12776
  ok: true,
12419
12777
  state: "updated",
@@ -12442,7 +12800,7 @@ function taskAmendArgv(request) {
12442
12800
  ...request.description === void 0 ? [] : request.description === null ? ["--clear-description"] : ["--description", request.description]
12443
12801
  ];
12444
12802
  }
12445
- async function amendTask(client, request) {
12803
+ async function amendTask(client, request, options = {}) {
12446
12804
  const invalid = requireTaskRef(request, amendTaskRequestSchema);
12447
12805
  if (invalid) return invalid;
12448
12806
  if (request.title === void 0 && request.description === void 0) return failureOutcome(opError("INVALID_REQUEST", { message: "Pass a new title, a new description, or description: null to clear it." }));
@@ -12453,7 +12811,7 @@ async function amendTask(client, request) {
12453
12811
  ...request.description === void 0 ? {} : { description: request.description }
12454
12812
  });
12455
12813
  if (!result.ok) return failureFromClientResult(result);
12456
- if (isHeldResponse(result.data)) return heldTaskWrite(request.target, result.data, "The amendment was not applied.", "retry_amend", taskAmendArgv(request));
12814
+ if (isHeldResponse(result.data)) return heldTaskWrite(request.target, result.data, "The amendment was not applied.", "retry_amend", taskAmendArgv(request), options.hints ?? "cli");
12457
12815
  return {
12458
12816
  ok: true,
12459
12817
  state: "amended",
@@ -12490,7 +12848,7 @@ async function taskHistory(client, request) {
12490
12848
  * and picks the task; a miss says whether the Server asserted the list is
12491
12849
  * complete, exactly as the CLI does.
12492
12850
  */
12493
- async function showTask(client, request) {
12851
+ async function showTask(client, request, options = {}) {
12494
12852
  const invalid = requireTaskRef(request);
12495
12853
  if (invalid) return invalid;
12496
12854
  const { target, taskNumber } = request;
@@ -12503,7 +12861,7 @@ async function showTask(client, request) {
12503
12861
  const task = tasks.find((candidate) => candidate.taskNumber === taskNumber);
12504
12862
  if (!task) return failureOutcome(opError("NOT_FOUND", {
12505
12863
  message: result.data.pagination?.mode === "complete" && result.data.pagination.truncated === false ? `task #${taskNumber} not found in ${target} (searched ${tasks.length} task(s), status=all; server asserts this list is complete)` : tasks.length === 0 ? `task #${taskNumber} not found in ${target}: the server returned 0 tasks and did not assert the list is complete. That can mean the channel has no tasks, or that the server failed to read them (it currently reports some read errors as an empty list), and this command cannot tell which — so this is not evidence that task #${taskNumber} does not exist` : `task #${taskNumber} not found in ${target} (searched ${tasks.length} task(s), status=all; this surface does NOT assert completeness — so this is "absent from what was returned", not "does not exist")`,
12506
- nextAction: `Check the number with \`raft task list --target "${target}" --status all\`.`
12864
+ nextAction: `Check the number with \`${formatHint(RAFT_HINTS.taskListAll(target), options.hints)}\`.`
12507
12865
  }));
12508
12866
  return {
12509
12867
  ok: true,
@@ -12520,7 +12878,7 @@ const convertMessageToTaskRequestSchema = requestSchema()(object({
12520
12878
  target: taskChannelSchema,
12521
12879
  messageId: string().describe("Full or short id of a top-level message in that channel.")
12522
12880
  }));
12523
- async function convertMessageToTask(client, request) {
12881
+ async function convertMessageToTask(client, request, options = {}) {
12524
12882
  const invalid = validateOpRequest(convertMessageToTaskRequestSchema, request);
12525
12883
  if (invalid) return invalid;
12526
12884
  if (!request.target?.trim() || !request.messageId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel target and a message id are required." }));
@@ -12537,13 +12895,8 @@ async function convertMessageToTask(client, request) {
12537
12895
  target: request.target,
12538
12896
  task
12539
12897
  },
12540
- next: {
12541
- kind: "post_in_task_thread",
12542
- command: `raft message send --target "${agentTaskThreadTarget(request.target, task.messageId)}"`,
12543
- args: { thread: agentTaskThreadTarget(request.target, task.messageId) },
12544
- why: "Follow up in the task's thread; it is unassigned until someone claims it."
12545
- },
12546
- text: formatAgentTaskConverted(request.target, task)
12898
+ next: taskThreadStep(request.target, task.messageId, "Follow up in the task's thread; it is unassigned until someone claims it.", options.hints),
12899
+ text: formatAgentTaskConverted(request.target, task, options.hints)
12547
12900
  };
12548
12901
  }
12549
12902
  async function deleteTask(client, request) {
@@ -12773,7 +13126,7 @@ function formatCurrentAgent(data) {
12773
13126
  return `${lines.join("\n")}\n\n`;
12774
13127
  }
12775
13128
  /** Server overview: runtime context, channels, agents, humans. */
12776
- function formatAgentServerInfo(data) {
13129
+ function formatAgentServerInfo(data, style = "cli") {
12777
13130
  let text = "## Server\n\n";
12778
13131
  const channels = data.channels ?? [];
12779
13132
  const agents = data.agents ?? [];
@@ -12781,8 +13134,14 @@ function formatAgentServerInfo(data) {
12781
13134
  text += formatRuntimeContext(data.runtimeContext);
12782
13135
  text += formatCurrentAgent(data);
12783
13136
  text += "### Channels\n";
12784
- text += "Visible public channels may appear even when `joined=false`. Private channels are shown only when you are a member; do not disclose private-channel names, membership, or content outside that channel. Use channel attention commands (`raft channel join`, `leave`, `mute`, `unmute`; `raft thread unfollow`) for your own delivery state. Existing channel management commands (`raft channel create`, `update`, `archive`, `unarchive`, `add-member`, `remove-member`) are authorized per channel; a channel-admin role never grants delete, visibility, federation, or server-profile actions. There is no Agent command for changing channel roles. Run any subcommand with `--help` for syntax.\n";
12785
- text += "Server-profile changes still use raft server update and remain server-role gated.\n";
13137
+ text += "Visible public channels may appear even when `joined=false`. Private channels are shown only when you are a member; do not disclose private-channel names, membership, or content outside that channel. ";
13138
+ text += style === "cli" ? `Use channel attention commands (\`${formatHintName(RAFT_HINTS.channelJoinName())}\`, \`leave\`, \`mute\`, \`unmute\`; \`${formatHintName(RAFT_HINTS.threadUnfollowName())}\`) for your own delivery state. Existing channel management commands (\`${formatHintName(RAFT_HINTS.channelCreateName())}\`, \`update\`, \`archive\`, \`unarchive\`, \`add-member\`, \`remove-member\`) are authorized per channel; a channel-admin role never grants delete, visibility, federation, or server-profile actions. There is no Agent command for changing channel roles. Run any subcommand with \`--help\` for syntax.\n` : `Use the channel attention tools (${[
13139
+ RAFT_HINTS.channelJoinName(),
13140
+ RAFT_HINTS.channelLeaveName(),
13141
+ RAFT_HINTS.channelMuteName(),
13142
+ RAFT_HINTS.channelUnmuteName()
13143
+ ].map((hint) => `\`${formatHintName(hint, style)}\``).join(", ")}; \`${formatHintName(RAFT_HINTS.threadUnfollowName(), style)}\`) for your own delivery state. Channel management (create, update, archive, unarchive, add-member, remove-member) has no tool: ${RAFT_NO_TOOL_WORDING}; a channel-admin role never grants delete, visibility, federation, or server-profile actions. There is no tool for changing channel roles.\n`;
13144
+ text += style === "cli" ? `Server-profile changes still use ${formatHintName(RAFT_HINTS.serverUpdateName())} and remain server-role gated.\n` : `Server-profile changes have no tool and remain server-role gated: ${RAFT_NO_TOOL_WORDING}.\n`;
12786
13145
  text += "Mute state is shown when the server provides it; otherwise it is omitted.\n";
12787
13146
  if (channels.length > 0) for (const t of channels) {
12788
13147
  const statusParts = [channelVisibility(t), t.joined ? "joined" : "not joined"];
@@ -12805,7 +13164,8 @@ function formatAgentServerInfo(data) {
12805
13164
  }
12806
13165
  else text += " (none)\n";
12807
13166
  text += "\n### Humans\n";
12808
- text += "To start a new DM: raft message send --target \"dm:@name\" <<'RAFTMSG' followed by the message body and RAFTMSG. To reply in an existing DM: reuse the target from received messages.\n";
13167
+ const newDm = formatHint(RAFT_HINTS.messageSend({ target: "dm:@name" }), style);
13168
+ text += style === "cli" ? `To start a new DM: ${newDm} <<'RAFTMSG' followed by the message body and RAFTMSG. To reply in an existing DM: reuse the target from received messages.\n` : `To start a new DM: ${newDm}. To reply in an existing DM: reuse the target from received messages.\n`;
12809
13169
  text += "Role labels show server-level owner/admin authority; no role label means ordinary member.\n";
12810
13170
  if (humans.length > 0) for (const u of humans) {
12811
13171
  const role = roleLabel(u.role);
@@ -12845,7 +13205,7 @@ function formatPageFooter(page) {
12845
13205
  return `${lines.join("\n")}\n`;
12846
13206
  }
12847
13207
  /** Single channel detail block. */
12848
- function formatAgentChannelInfo(channel, memberCounts) {
13208
+ function formatAgentChannelInfo(channel, memberCounts, style = "cli") {
12849
13209
  const lines = ["## Channel", ""];
12850
13210
  lines.push(`Channel: ${agentChannelRef(channel.name, channel.type)}`);
12851
13211
  if (channel.id) lines.push(`ID: ${channel.id}`);
@@ -12865,11 +13225,11 @@ function formatAgentChannelInfo(channel, memberCounts) {
12865
13225
  lines.push(`Members: ${agents + humans} (${agents} agents, ${humans} humans)`);
12866
13226
  }
12867
13227
  lines.push("");
12868
- lines.push(`More: raft channel members "${agentChannelRef(channel.name, channel.type)}"`);
13228
+ lines.push(`More: ${formatHint(RAFT_HINTS.channelMembers(agentChannelRef(channel.name, channel.type)), style)}`);
12869
13229
  return `${lines.join("\n")}\n`;
12870
13230
  }
12871
13231
  /** Compact server summary. */
12872
- function formatAgentServerSummary(data) {
13232
+ function formatAgentServerSummary(data, style = "cli") {
12873
13233
  const channels = data.channels ?? [];
12874
13234
  const agents = data.agents ?? [];
12875
13235
  const humans = data.humans ?? [];
@@ -12882,13 +13242,15 @@ function formatAgentServerSummary(data) {
12882
13242
  `Humans: ${humans.length}`,
12883
13243
  "",
12884
13244
  "Narrow queries:",
12885
- "- raft server info --channels",
12886
- "- raft server info --agents",
12887
- "- raft server info --humans",
12888
- "- raft channel info <name>",
12889
- "- raft user info <name>",
13245
+ ...[
13246
+ RAFT_HINTS.serverInfo({ view: "channels" }),
13247
+ RAFT_HINTS.serverInfo({ view: "agents" }),
13248
+ RAFT_HINTS.serverInfo({ view: "humans" }),
13249
+ RAFT_HINTS.channelInfo(),
13250
+ RAFT_HINTS.userInfo()
13251
+ ].map((hint) => `- ${formatHint(hint, style)}`),
12890
13252
  "",
12891
- "Full dump: raft server info --full"
13253
+ `Full dump: ${formatHint(RAFT_HINTS.serverInfo({ view: "full" }), style)}`
12892
13254
  ].join("\n")}\n`;
12893
13255
  }
12894
13256
  /** Channel listing page. */
@@ -13169,7 +13531,7 @@ const unfollowThreadRequestSchema = requestSchema()(object({
13169
13531
  reason: string().optional().describe("Short reason, kept with the unfollow.")
13170
13532
  }));
13171
13533
  const INVALID_CHANNEL_TARGET = "Target must be a regular channel in the form '#channel-name'. DMs and thread targets are not supported.";
13172
- async function resolveRegularChannel(client, target) {
13534
+ async function resolveRegularChannel(client, target, options) {
13173
13535
  const name = parseRaftRegularChannelTarget$1(target ?? "");
13174
13536
  if (!name) return failureOutcome(opError("INVALID_REQUEST", { message: INVALID_CHANNEL_TARGET }));
13175
13537
  const info = await client.server.info();
@@ -13177,7 +13539,7 @@ async function resolveRegularChannel(client, target) {
13177
13539
  const channel = info.data.channels.find((candidate) => candidate.name === name);
13178
13540
  if (!channel) return failureOutcome(opError("NOT_FOUND", {
13179
13541
  message: `Channel not found: ${target}`,
13180
- nextAction: "List visible channels with `raft server info --channels`; private channels need a human to add you."
13542
+ nextAction: `List visible channels with \`${formatHint(RAFT_HINTS.serverInfo({ view: "channels" }), options.hints)}\`; private channels need a human to add you.`
13181
13543
  }));
13182
13544
  return {
13183
13545
  id: channel.id,
@@ -13185,7 +13547,7 @@ async function resolveRegularChannel(client, target) {
13185
13547
  joined: channel.joined
13186
13548
  };
13187
13549
  }
13188
- async function joinChannel(client, request) {
13550
+ async function joinChannel(client, request, options = {}) {
13189
13551
  const invalid = validateOpRequest(channelTargetRequestSchema, request);
13190
13552
  if (invalid) return invalid;
13191
13553
  const result = await joinRaftChannelByTarget$1(client, { target: request.target });
@@ -13211,19 +13573,14 @@ async function joinChannel(client, request) {
13211
13573
  target: result.data.target,
13212
13574
  channelId: result.data.channelId
13213
13575
  },
13214
- next: {
13215
- kind: "read_target",
13216
- command: `raft message read --target "${result.data.target}"`,
13217
- args: { target: result.data.target },
13218
- why: "Read the channel before posting."
13219
- },
13576
+ next: hintStep("read_target", RAFT_HINTS.messageRead({ target: result.data.target }), "Read the channel before posting.", options.hints, { target: result.data.target }),
13220
13577
  text: state === "joined" ? `Joined ${result.data.target}.` : `Already a member of ${result.data.target}.`
13221
13578
  };
13222
13579
  }
13223
- async function leaveChannel(client, request) {
13580
+ async function leaveChannel(client, request, options = {}) {
13224
13581
  const invalid = validateOpRequest(channelTargetRequestSchema, request);
13225
13582
  if (invalid) return invalid;
13226
- const channel = await resolveRegularChannel(client, request.target);
13583
+ const channel = await resolveRegularChannel(client, request.target, options);
13227
13584
  if ("ok" in channel) return channel;
13228
13585
  if (!channel.joined) return {
13229
13586
  ok: true,
@@ -13254,10 +13611,10 @@ async function leaveChannel(client, request) {
13254
13611
  function formatSeq(value) {
13255
13612
  return value == null ? "none" : String(value);
13256
13613
  }
13257
- async function setChannelMute(client, request, action) {
13614
+ async function setChannelMute(client, request, action, options) {
13258
13615
  const invalid = validateOpRequest(channelTargetRequestSchema, request);
13259
13616
  if (invalid) return invalid;
13260
- const channel = await resolveRegularChannel(client, request.target);
13617
+ const channel = await resolveRegularChannel(client, request.target, options);
13261
13618
  if ("ok" in channel) return channel;
13262
13619
  const result = action === "mute" ? await client.channels.mute({ channelId: channel.id }, {}) : await client.channels.unmute({ channelId: channel.id });
13263
13620
  if (!result.ok) return failureFromClientResult(result);
@@ -13292,11 +13649,11 @@ async function setChannelMute(client, request, action) {
13292
13649
  text: lines.join("\n")
13293
13650
  };
13294
13651
  }
13295
- function muteChannel(client, request) {
13296
- return setChannelMute(client, request, "mute");
13652
+ function muteChannel(client, request, options = {}) {
13653
+ return setChannelMute(client, request, "mute", options);
13297
13654
  }
13298
- function unmuteChannel(client, request) {
13299
- return setChannelMute(client, request, "unmute");
13655
+ function unmuteChannel(client, request, options = {}) {
13656
+ return setChannelMute(client, request, "unmute", options);
13300
13657
  }
13301
13658
  async function channelMembers(client, request) {
13302
13659
  const invalid = validateOpRequest(channelMembersRequestSchema, request);
@@ -13346,7 +13703,7 @@ const channelInfoRequestSchema = requestSchema()(object({ target: string().descr
13346
13703
  * `raft channel info <target>`: the channel's facts from `server.info`, plus
13347
13704
  * member counts from its roster when the Server shows it.
13348
13705
  */
13349
- async function channelInfo(client, request) {
13706
+ async function channelInfo(client, request, options = {}) {
13350
13707
  const invalid = validateOpRequest(channelInfoRequestSchema, request);
13351
13708
  if (invalid) return invalid;
13352
13709
  const trimmed = request.target.trim();
@@ -13358,7 +13715,10 @@ async function channelInfo(client, request) {
13358
13715
  const channel = info.data.channels.find((candidate) => candidate.name === name);
13359
13716
  if (!channel) return failureOutcome(opError("NOT_FOUND", {
13360
13717
  message: `Channel not found or not visible: ${input}`,
13361
- nextAction: "Run `raft server info --channels --query <name>` to inspect visible channels, or ask a channel member to add you if this is private."
13718
+ nextAction: `Run \`${formatHint(RAFT_HINTS.serverInfo({
13719
+ view: "channels",
13720
+ query: true
13721
+ }), options.hints)}\` to inspect visible channels, or ask a channel member to add you if this is private.`
13362
13722
  }));
13363
13723
  const members = await client.channels.members({ channel: `#${name}` });
13364
13724
  const memberCounts = members.ok ? {
@@ -13373,7 +13733,7 @@ async function channelInfo(client, request) {
13373
13733
  memberCounts
13374
13734
  },
13375
13735
  next: null,
13376
- text: formatAgentChannelInfo(channel, memberCounts)
13736
+ text: formatAgentChannelInfo(channel, memberCounts, options.hints)
13377
13737
  };
13378
13738
  }
13379
13739
  //#endregion
@@ -13388,45 +13748,54 @@ const serverInfoRequestSchema = requestSchema()(object({
13388
13748
  ]).optional().describe("summary (default): counts; full: the whole overview; channels / agents / humans: one paged section."),
13389
13749
  offset: number$1().optional().describe("Section paging: rows to skip."),
13390
13750
  limit: number$1().optional().describe("Section paging: rows per page (default 50)."),
13391
- joined: boolean().optional().describe("channels view only: only channels you have joined.")
13751
+ joined: boolean().optional().describe("channels view only: only channels you have joined."),
13752
+ query: string().optional().describe("channels / agents / humans view only: keep rows whose name, description, or other visible text contains this (case-insensitive).")
13392
13753
  }));
13393
13754
  const showProfileRequestSchema = requestSchema()(object({ target: string().optional().describe("`@handle` of someone else; omit for your own profile.") }));
13394
13755
  /** The Agent API's profile body schema; at least one field is required (checked by the operation). */
13395
13756
  const updateProfileRequestSchema = agentApiProfileUpdateBodySchema;
13396
- function nextCommand(section, request, offset, limit, total) {
13757
+ /** The next page of a section, or null on the last page (`--query` / `--joined` carried, in the CLI's order). */
13758
+ function nextPageArgs(section, request, offset, limit, total) {
13397
13759
  const nextOffset = offset + limit;
13398
- if (nextOffset >= total) return void 0;
13399
- const parts = [
13400
- "raft server info",
13401
- `--${section}`,
13402
- `--offset ${nextOffset}`,
13403
- `--limit ${limit}`
13404
- ];
13405
- if (section === "channels" && request.joined) parts.push("--joined");
13406
- return parts.join(" ");
13760
+ if (nextOffset >= total) return null;
13761
+ const query = request.query?.trim() || void 0;
13762
+ const joined = section === "channels" && request.joined === true ? true : void 0;
13763
+ return {
13764
+ view: section,
13765
+ offset: nextOffset,
13766
+ limit,
13767
+ ...query === void 0 ? {} : { query },
13768
+ ...joined ? { joined } : {}
13769
+ };
13407
13770
  }
13408
- async function serverInfo(client, request = {}) {
13771
+ /** The CLI's `--query` match: any string field contains the needle, case-insensitively. */
13772
+ function includesQuery(row, query) {
13773
+ const needle = query?.trim().toLowerCase();
13774
+ if (!needle) return true;
13775
+ return Object.values(row).some((value) => typeof value === "string" && value.toLowerCase().includes(needle));
13776
+ }
13777
+ async function serverInfo(client, request = {}, options = {}) {
13409
13778
  const invalid = validateOpRequest(serverInfoRequestSchema, request);
13410
13779
  if (invalid) return invalid;
13780
+ const style = options.hints ?? "cli";
13411
13781
  const result = await client.server.info();
13412
13782
  if (!result.ok) return failureFromClientResult(result);
13413
13783
  const server = result.data;
13414
13784
  const view = request.view ?? "summary";
13415
- if (view === "summary") return {
13416
- ok: true,
13417
- state: "info",
13418
- data: {
13419
- view,
13420
- server,
13421
- page: null
13422
- },
13423
- next: {
13424
- kind: "list_channels",
13425
- command: "raft server info --channels",
13426
- why: "The summary only counts; list a section to see names."
13427
- },
13428
- text: formatAgentServerSummary(server)
13429
- };
13785
+ if (view === "summary") {
13786
+ const next = hintStep("list_channels", RAFT_HINTS.serverInfo({ view: "channels" }), "The summary only counts; list a section to see names.", style);
13787
+ return {
13788
+ ok: true,
13789
+ state: "info",
13790
+ data: {
13791
+ view,
13792
+ server,
13793
+ page: null
13794
+ },
13795
+ next,
13796
+ text: formatAgentServerSummary(server, style)
13797
+ };
13798
+ }
13430
13799
  if (view === "full") return {
13431
13800
  ok: true,
13432
13801
  state: "info",
@@ -13436,30 +13805,23 @@ async function serverInfo(client, request = {}) {
13436
13805
  page: null
13437
13806
  },
13438
13807
  next: null,
13439
- text: formatAgentServerInfo(server)
13808
+ text: formatAgentServerInfo(server, style)
13440
13809
  };
13441
13810
  const limit = Math.max(1, Math.trunc(request.limit ?? 50));
13442
13811
  const offset = Math.max(0, Math.trunc(request.offset ?? 0));
13443
- const rows = view === "channels" ? server.channels.filter((c) => !request.joined || c.joined) : view === "agents" ? server.agents : server.humans;
13812
+ const rows = (view === "channels" ? server.channels.filter((c) => !request.joined || c.joined) : view === "agents" ? server.agents : server.humans).filter((row) => includesQuery(row, request.query));
13444
13813
  const pageRows = rows.slice(offset, offset + limit);
13814
+ const nextArgs = nextPageArgs(view, request, offset, limit, rows.length);
13815
+ const nextHint = nextArgs ? RAFT_HINTS.serverInfo(nextArgs) : null;
13445
13816
  const page = {
13446
13817
  section: view,
13447
13818
  total: rows.length,
13448
13819
  offset,
13449
13820
  limit,
13450
- nextCommand: nextCommand(view, request, offset, limit, rows.length)
13821
+ nextCommand: nextHint ? formatHint(nextHint, style) : void 0
13451
13822
  };
13452
13823
  const text = view === "channels" ? formatAgentServerChannels(pageRows, page) : view === "agents" ? formatAgentServerAgents(pageRows, page) : formatAgentServerHumans(pageRows, page);
13453
- const next = page.nextCommand ? {
13454
- kind: "next_page",
13455
- command: page.nextCommand,
13456
- args: {
13457
- view,
13458
- offset: offset + limit,
13459
- limit
13460
- },
13461
- why: "More rows exist; one page is one page."
13462
- } : null;
13824
+ const next = nextArgs && nextHint ? hintStep("next_page", nextHint, "More rows exist; one page is one page.", style, nextArgs) : null;
13463
13825
  return {
13464
13826
  ok: true,
13465
13827
  state: "info",
@@ -13509,7 +13871,7 @@ const userInfoRequestSchema = requestSchema()(object({
13509
13871
  * their memberships among one page of the visible channels, checked one
13510
13872
  * channel roster at a time (a rejected roster is skipped and counted).
13511
13873
  */
13512
- async function userInfo(client, request) {
13874
+ async function userInfo(client, request, options = {}) {
13513
13875
  const invalid = validateOpRequest(userInfoRequestSchema, request);
13514
13876
  if (invalid) return invalid;
13515
13877
  const trimmed = request.name.trim();
@@ -13530,7 +13892,13 @@ async function userInfo(client, request) {
13530
13892
  } : null;
13531
13893
  if (!user) return failureOutcome(opError("NOT_FOUND", {
13532
13894
  message: `User not found or not visible: @${name}`,
13533
- nextAction: "Run `raft server info --agents --query <name>` or `raft server info --humans --query <name>` to inspect visible users."
13895
+ nextAction: `Run \`${formatHint(RAFT_HINTS.serverInfo({
13896
+ view: "agents",
13897
+ query: true
13898
+ }), options.hints)}\` or \`${formatHint(RAFT_HINTS.serverInfo({
13899
+ view: "humans",
13900
+ query: true
13901
+ }), options.hints)}\` to inspect visible users.`
13534
13902
  }));
13535
13903
  const visibleChannels = info.data.channels;
13536
13904
  const memberships = [];
@@ -13549,22 +13917,22 @@ async function userInfo(client, request) {
13549
13917
  });
13550
13918
  }
13551
13919
  const nextOffset = offset + limit;
13920
+ const nextHint = nextOffset < visibleChannels.length ? RAFT_HINTS.userInfo({
13921
+ name,
13922
+ offset: nextOffset,
13923
+ limit
13924
+ }) : null;
13552
13925
  const page = {
13553
13926
  total: visibleChannels.length,
13554
13927
  offset,
13555
13928
  limit,
13556
- nextCommand: nextOffset < visibleChannels.length ? `raft user info @${name} --offset ${nextOffset} --limit ${limit}` : void 0
13929
+ nextCommand: nextHint ? formatHint(nextHint, options.hints) : void 0
13557
13930
  };
13558
- const next = page.nextCommand ? {
13559
- kind: "next_page",
13560
- command: page.nextCommand,
13561
- args: {
13562
- name: `@${name}`,
13563
- offset: nextOffset,
13564
- limit
13565
- },
13566
- why: "Only one page of visible channels was inspected for memberships."
13567
- } : null;
13931
+ const next = nextHint ? hintStep("next_page", nextHint, "Only one page of visible channels was inspected for memberships.", options.hints, {
13932
+ name: `@${name}`,
13933
+ offset: nextOffset,
13934
+ limit
13935
+ }) : null;
13568
13936
  return {
13569
13937
  ok: true,
13570
13938
  state: "info",
@@ -13581,8 +13949,9 @@ async function userInfo(client, request) {
13581
13949
  //#endregion
13582
13950
  //#region ../shared/src/agentText/attachments.ts
13583
13951
  /** Upload receipt with attachment id and send-usage hint. */
13584
- function formatAgentAttachmentUploaded(attachment) {
13585
- return `File uploaded: ${attachment.filename} (${(attachment.sizeBytes / 1024).toFixed(1)}KB)\nAttachment ID: ${attachment.id}\n\nUse this ID with raft message send --attachment-id ${attachment.id} to include it in a message.\n`;
13952
+ function formatAgentAttachmentUploaded(attachment, style = "cli") {
13953
+ const send = formatHint(RAFT_HINTS.messageSend({ attachmentId: attachment.id }), style);
13954
+ return `File uploaded: ${attachment.filename} (${(attachment.sizeBytes / 1024).toFixed(1)}KB)\nAttachment ID: ${attachment.id}\n\nUse this ID with ${send} to include it in a message.\n`;
13586
13955
  }
13587
13956
  function anchorSummary(anchor) {
13588
13957
  if (anchor.type === "md-section") {
@@ -13624,6 +13993,7 @@ const attachmentCommentsRequestSchema = requestSchema()(object({
13624
13993
  attachmentId: string().describe("The attachment id."),
13625
13994
  limit: number$1().int().positive().optional().describe("Maximum comments to return.")
13626
13995
  }));
13996
+ const downloadAttachmentUrlRequestSchema = requestSchema()(object({ attachmentId: string().describe("The attachment id, as message lines show it (`id:…`).") }));
13627
13997
  /** Compatibility fallback for Servers that predate the capability endpoint (the CLI's constant). */
13628
13998
  const AGENT_ATTACHMENT_UPLOAD_FALLBACK_MAX_BYTES = 52428800;
13629
13999
  const FILENAME_MIME_MAP = {
@@ -13676,7 +14046,7 @@ function inferAttachmentMimeType(filename, bytes, explicit) {
13676
14046
  * `POST /upload` below the Server's direct-upload threshold, an upload session
13677
14047
  * (create → PUT to a presigned URL → complete) at or above it.
13678
14048
  */
13679
- async function uploadAttachment(client, transport, request) {
14049
+ async function uploadAttachment(client, transport, request, options = {}) {
13680
14050
  if (!request.target?.trim() || !request.filename?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and a filename are required to upload." }));
13681
14051
  if (!(request.bytes instanceof Uint8Array) || request.bytes.byteLength === 0) return failureOutcome(opError("INVALID_REQUEST", { message: "Refusing to upload a 0-byte attachment." }));
13682
14052
  if (request.mimeType !== void 0 && !MIME_TYPE_RE.test(request.mimeType.trim())) return failureOutcome(opError("INVALID_REQUEST", { message: `mimeType must look like type/subtype, got: ${request.mimeType}` }));
@@ -13688,7 +14058,7 @@ async function uploadAttachment(client, transport, request) {
13688
14058
  if (!resolved.ok) return failureFromClientResult(resolved);
13689
14059
  const channelId = resolved.data.channelId;
13690
14060
  const mimeType = inferAttachmentMimeType(request.filename, request.bytes, request.mimeType);
13691
- if (threshold !== null && request.bytes.byteLength >= threshold) return uploadThroughSession(client, transport, request, channelId, mimeType);
14061
+ if (threshold !== null && request.bytes.byteLength >= threshold) return uploadThroughSession(client, transport, request, channelId, mimeType, options);
13692
14062
  const copy = new Uint8Array(request.bytes.byteLength);
13693
14063
  copy.set(request.bytes);
13694
14064
  const form = new FormData();
@@ -13728,9 +14098,9 @@ async function uploadAttachment(client, transport, request) {
13728
14098
  }
13729
14099
  const parsed = agentApiContract.attachmentUpload.response.body.safeParse(body);
13730
14100
  if (!parsed.success) return failureOutcome(opError("INVALID_RESPONSE"));
13731
- return uploadedOutcome(request.target, parsed.data, mimeType);
14101
+ return uploadedOutcome(request.target, parsed.data, mimeType, options);
13732
14102
  }
13733
- function uploadedOutcome(target, data, mimeType) {
14103
+ function uploadedOutcome(target, data, mimeType, options) {
13734
14104
  return {
13735
14105
  ok: true,
13736
14106
  state: "uploaded",
@@ -13739,16 +14109,14 @@ function uploadedOutcome(target, data, mimeType) {
13739
14109
  target,
13740
14110
  mimeType: data.mimeType ?? mimeType
13741
14111
  },
13742
- next: {
13743
- kind: "send_with_attachment",
13744
- command: `raft message send --target "${target}" --attachment-id ${data.id}`,
13745
- args: {
13746
- target,
13747
- attachmentIds: [data.id]
13748
- },
13749
- why: "The upload alone posts nothing; send a message that links the attachment id."
13750
- },
13751
- text: formatAgentAttachmentUploaded(data)
14112
+ next: hintStep("send_with_attachment", RAFT_HINTS.messageSend({
14113
+ target,
14114
+ attachmentId: data.id
14115
+ }), "The upload alone posts nothing; send a message that links the attachment id.", options.hints, {
14116
+ target,
14117
+ attachmentIds: [data.id]
14118
+ }),
14119
+ text: formatAgentAttachmentUploaded(data, options.hints)
13752
14120
  };
13753
14121
  }
13754
14122
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -13759,7 +14127,7 @@ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13759
14127
  * verifies against the object store. A PUT that definitely failed cancels the
13760
14128
  * session; a PUT whose outcome is unknown leaves it for completion to verify.
13761
14129
  */
13762
- async function uploadThroughSession(client, transport, request, channelId, mimeType) {
14130
+ async function uploadThroughSession(client, transport, request, channelId, mimeType, options) {
13763
14131
  const created = await client.attachments.createUploadSession({
13764
14132
  channelId,
13765
14133
  filename: request.filename,
@@ -13806,7 +14174,7 @@ async function uploadThroughSession(client, transport, request, channelId, mimeT
13806
14174
  const completed = await client.attachments.completeUploadSession({ uploadId });
13807
14175
  if (completed.ok) {
13808
14176
  const attachment = completed.data.attachment;
13809
- return uploadedOutcome(request.target, attachment, mimeType);
14177
+ return uploadedOutcome(request.target, attachment, mimeType, options);
13810
14178
  }
13811
14179
  const code = completed.error.kind === "http" ? completed.error.errorCode : null;
13812
14180
  if (!(code === "UPLOAD_OBJECT_NOT_FOUND" || code === "UPLOAD_VERIFICATION_IN_PROGRESS") || attempt === 2) return failureFromClientResult(completed);
@@ -13830,6 +14198,46 @@ async function downloadAttachment(client, request) {
13830
14198
  text: `Downloaded attachment ${request.attachmentId.slice(0, 8)} (${bytes.byteLength} bytes).`
13831
14199
  };
13832
14200
  }
14201
+ /**
14202
+ * Mint a short-lived URL for an attachment's bytes, for runtimes whose tools
14203
+ * cannot return binary data: the runtime (not the model) fetches `url` before
14204
+ * `expiresAt`. The URL is a bearer capability; do not log it. A Server whose
14205
+ * storage cannot presign answers 409 `download_url_unavailable`; that failure's
14206
+ * `next` points at the binary download (`attachments.download`).
14207
+ */
14208
+ async function downloadAttachmentUrl(client, request, options = {}) {
14209
+ const invalid = validateOpRequest(downloadAttachmentUrlRequestSchema, request);
14210
+ if (invalid) return invalid;
14211
+ const attachmentId = request.attachmentId?.trim();
14212
+ if (!attachmentId) return failureOutcome(opError("INVALID_REQUEST", { message: "An attachment id is required." }));
14213
+ const result = await client.attachments.downloadUrl({ attachmentId });
14214
+ if (!result.ok) {
14215
+ const failure = failureFromClientResult(result);
14216
+ if (failure.error.serverCode !== "download_url_unavailable") return failure;
14217
+ const download = RAFT_HINTS.attachmentDownload(attachmentId);
14218
+ const nextAction = `This Server's storage cannot mint download URLs; download the bytes instead: \`${formatHint(download, options.hints)}\`.`;
14219
+ return {
14220
+ ...failureOutcome({
14221
+ ...failure.error,
14222
+ nextAction
14223
+ }),
14224
+ next: hintStep("download_bytes", download, nextAction, options.hints, { attachmentId })
14225
+ };
14226
+ }
14227
+ const { url, expiresAt, filename, mimeType } = result.data;
14228
+ return {
14229
+ ok: true,
14230
+ state: "url",
14231
+ data: {
14232
+ url,
14233
+ expiresAt,
14234
+ filename,
14235
+ mimeType
14236
+ },
14237
+ next: null,
14238
+ text: `Download URL for ${filename} (${mimeType}), valid until ${expiresAt}:\n${url}`
14239
+ };
14240
+ }
13833
14241
  async function attachmentComments(client, request) {
13834
14242
  const invalid = validateOpRequest(attachmentCommentsRequestSchema, request);
13835
14243
  if (invalid) return invalid;
@@ -13978,14 +14386,15 @@ function renderSearchPreview(content, query) {
13978
14386
  ].join("");
13979
14387
  }
13980
14388
  /** Search results with <match>/<omit /> preview markup. */
13981
- function formatAgentSearchResults(query, data, offset, sort, limit) {
14389
+ function formatAgentSearchResults(query, data, offset, sort, limit, style = "cli") {
13982
14390
  const trimmedQuery = query.trim();
14391
+ const flag = (name) => formatHintFlag(name, name, style);
13983
14392
  const oldestShown = data.results?.length ? data.results[data.results.length - 1]?.createdAt : void 0;
13984
- const nextPageHint = sort === "recent" && oldestShown ? `page with --before ${oldestShown} (pages OLDER only; NOT a complete traversal: stored times are microsecond but this key is millisecond, so rows inside the boundary millisecond can be skipped, and a full page sharing one timestamp can repeat indefinitely; copy the key verbatim)` : `page with --offset ${(offset ?? 0) + (data.results?.length ?? 0)}`;
14393
+ const nextPageHint = sort === "recent" && oldestShown ? `page with ${flag("before")} ${oldestShown} (pages OLDER only; NOT a complete traversal: stored times are microsecond but this key is millisecond, so rows inside the boundary millisecond can be skipped, and a full page sharing one timestamp can repeat indefinitely; copy the key verbatim)` : `page with ${flag("offset")} ${(offset ?? 0) + (data.results?.length ?? 0)}`;
13985
14394
  const effectiveLimit = Math.min(limit ?? 20, 50);
13986
14395
  const countEqualsLimit = (data.results?.length ?? 0) === effectiveLimit;
13987
- const limitSource = limit === void 0 ? `the server default of ${effectiveLimit}` : limit > 50 ? `the server cap of ${effectiveLimit} (your --limit ${limit} was clamped)` : `the --limit ${effectiveLimit} that was requested`;
13988
- const atCapRemedy = effectiveLimit < 50 ? "re-run with a higher --limit to tell the two apart" : `this is already the server's maximum page, so completeness CANNOT be determined from this result: a higher --limit is clamped back to ${effectiveLimit} and paging will not reveal it; narrow the query (add --sender, --target, --after or --before) until fewer than ${effectiveLimit} results match`;
14396
+ const limitSource = limit === void 0 ? `the server default of ${effectiveLimit}` : limit > 50 ? `the server cap of ${effectiveLimit} (your ${flag("limit")} ${limit} was clamped)` : `the ${flag("limit")} ${effectiveLimit} that was requested`;
14397
+ const atCapRemedy = effectiveLimit < 50 ? `re-run with a higher ${flag("limit")} to tell the two apart` : `this is already the server's maximum page, so completeness CANNOT be determined from this result: a higher ${flag("limit")} is clamped back to ${effectiveLimit} and paging will not reveal it; narrow the query (add ${flag("sender")}, ${flag("target")}, ${flag("after")} or ${flag("before")}) until fewer than ${effectiveLimit} results match`;
13989
14398
  const truncation = data.hasMore === true ? `truncated=true · more results exist, ${nextPageHint}` : data.hasMore === false ? countEqualsLimit ? `truncated=unknown · server reported hasMore=false, but this page returned exactly ${limitSource}, which is also what a capped page returns; ${atCapRemedy}` : "truncated=false" : "truncated=unknown · server did not report hasMore";
13990
14399
  if (!data.results || data.results.length === 0) return `No search results. (${truncation})`;
13991
14400
  const formatted = data.results.map((result) => {
@@ -14034,7 +14443,7 @@ const reactRequestSchema = requestSchema()(object({
14034
14443
  messageId: string().describe("Full or short message id."),
14035
14444
  emoji: string().describe("One reaction emoji.")
14036
14445
  }));
14037
- async function searchMessages(client, request) {
14446
+ async function searchMessages(client, request, options = {}) {
14038
14447
  const invalid = validateOpRequest(searchMessagesRequestSchema, request);
14039
14448
  if (invalid) return invalid;
14040
14449
  if (!request.query?.trim() && !request.target && !request.sender) return failureOutcome(opError("INVALID_REQUEST", { message: "Pass a query, or filter by target or sender." }));
@@ -14056,11 +14465,16 @@ async function searchMessages(client, request) {
14056
14465
  results: data.results,
14057
14466
  hasMore
14058
14467
  };
14468
+ const nextArgs = {
14469
+ ...request,
14470
+ offset: (request.offset ?? 0) + data.results.length
14471
+ };
14059
14472
  const next = hasMore ? {
14060
14473
  kind: "next_search_page",
14061
- args: {
14062
- ...request,
14063
- offset: (request.offset ?? 0) + data.results.length
14474
+ args: nextArgs,
14475
+ operation: {
14476
+ name: "messages.search",
14477
+ args: nextArgs
14064
14478
  },
14065
14479
  why: "More results exist; page on, or narrow the query."
14066
14480
  } : null;
@@ -14072,31 +14486,32 @@ async function searchMessages(client, request) {
14072
14486
  text: formatAgentSearchResults(page.query, {
14073
14487
  results: data.results,
14074
14488
  ...hasMore === null ? {} : { hasMore }
14075
- }, request.offset, request.sort, request.limit)
14489
+ }, request.offset, request.sort, request.limit, options.hints)
14076
14490
  };
14077
14491
  }
14078
- async function resolveMessage(client, request) {
14492
+ async function resolveMessage(client, request, options = {}) {
14079
14493
  const invalid = validateOpRequest(resolveMessageRequestSchema, request);
14080
14494
  if (invalid) return invalid;
14081
14495
  if (!request.messageId?.trim()) return failureOutcome(opError("INVALID_REQUEST", { message: "A message id is required." }));
14082
14496
  const result = await client.messages.resolve({ msgId: request.messageId });
14083
14497
  if (!result.ok) return failureFromClientResult(result);
14084
- const message = projectRaftMessage(result.data.message);
14498
+ const message = projectRaftMessage(result.data.message, options.hints);
14085
14499
  if (!message) return failureOutcome(opError("INVALID_RESPONSE", { message: "The resolved message carries no conversation identity." }));
14086
14500
  return {
14087
14501
  ok: true,
14088
14502
  state: "message",
14089
14503
  data: message,
14090
- next: {
14091
- kind: "read_target",
14092
- command: `raft message read --target "${message.target}" --around ${message.shortId ?? request.messageId}`,
14093
- args: {
14094
- target: message.target,
14095
- around: message.id ?? request.messageId
14096
- },
14097
- why: "Read the surrounding context before acting on one message."
14098
- },
14099
- text: formatAgentMessages([toAgentMessageLike(message.raw)])
14504
+ next: hintStep("read_target", RAFT_HINTS.messageRead({
14505
+ target: message.target,
14506
+ around: {
14507
+ shown: message.shortId ?? request.messageId,
14508
+ id: message.id ?? request.messageId
14509
+ }
14510
+ }), "Read the surrounding context before acting on one message.", options.hints, {
14511
+ target: message.target,
14512
+ around: message.id ?? request.messageId
14513
+ }),
14514
+ text: formatAgentMessages([toAgentMessageLike(message.raw)], options.hints)
14100
14515
  };
14101
14516
  }
14102
14517
  async function reactToMessage(client, request, action = "add") {
@@ -14126,17 +14541,20 @@ function normalizeAgentMentionAction(action) {
14126
14541
  if (action === "add" || action === "invite") return "add";
14127
14542
  return null;
14128
14543
  }
14129
- function formatActionCommands(action) {
14544
+ function actionVerbs(action) {
14130
14545
  const verbs = action.availableActions.map(normalizeAgentMentionAction).filter((verb) => verb !== null);
14131
- return Array.from(new Set(verbs)).map((verb) => ` ${verb}: raft mention ${verb} ${action.resolutionId}`);
14546
+ return Array.from(new Set(verbs));
14547
+ }
14548
+ function formatActionCommands(action, style) {
14549
+ return actionVerbs(action).map((verb) => ` ${verb}: ${formatHint(RAFT_HINTS.mentionAction(verb, action.resolutionId), style)}`);
14132
14550
  }
14133
14551
  function formatAuthoredMentionToken(targetHandle) {
14134
14552
  return targetHandle.startsWith("@") ? targetHandle : `@${targetHandle}`;
14135
14553
  }
14136
14554
  const PENDING_MENTION_ACTION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14137
14555
  /** Per-token mention recovery command line; null when the id is not a UUID (fail closed). */
14138
- function formatAgentMentionNotifyRecoveryCommand(resolutionId) {
14139
- return PENDING_MENTION_ACTION_ID_RE.test(resolutionId) ? `raft mention notify ${resolutionId}` : null;
14556
+ function formatAgentMentionNotifyRecoveryCommand(resolutionId, style = "cli") {
14557
+ return PENDING_MENTION_ACTION_ID_RE.test(resolutionId) ? formatHint(RAFT_HINTS.mentionAction("notify", resolutionId), style) : null;
14140
14558
  }
14141
14559
  function toAgentSenderPendingMentionAction(action) {
14142
14560
  return {
@@ -14193,22 +14611,24 @@ function normalizeAgentMentionActionResults(data) {
14193
14611
  })).filter((item) => item.resolutionId.length > 0);
14194
14612
  }
14195
14613
  const PENDING_DEFAULT_LIMIT = 50;
14196
- function pendingPageVerdict(hasMore, limit, shown) {
14197
- const asked = limit === void 0 ? "server default 50" : `--limit ${limit}`;
14198
- if (hasMore === true) return (limit ?? PENDING_DEFAULT_LIMIT) >= 100 ? `shown ${shown}, ${asked} — truncated=true · more pending actions exist AND this is the server's maximum page (100); this route has no offset or cursor, so the rest cannot be reached from here — resolve what you can and re-run; rows with no available actions are not actionable here and will remain until they expire` : `shown ${shown}, ${asked} — truncated=true · more pending actions exist; raise --limit (server caps at 100) or resolve these first and re-run`;
14614
+ function pendingPageVerdict(hasMore, limit, shown, style) {
14615
+ const limitFlag = formatHintFlag("limit", "limit", style);
14616
+ const asked = limit === void 0 ? "server default 50" : `${limitFlag} ${limit}`;
14617
+ if (hasMore === true) return (limit ?? PENDING_DEFAULT_LIMIT) >= 100 ? `shown ${shown}, ${asked} — truncated=true · more pending actions exist AND this is the server's maximum page (100); this route has no offset or cursor, so the rest cannot be reached from here — resolve what you can and re-run; rows with no available actions are not actionable here and will remain until they expire` : `shown ${shown}, ${asked} — truncated=true · more pending actions exist; raise ${limitFlag} (server caps at 100) or resolve these first and re-run`;
14199
14618
  if (hasMore === false) return `shown ${shown}, ${asked} — truncated=false · this is the complete list`;
14200
14619
  return `shown ${shown}, ${asked} — truncated=unknown · server did not report has_more, so completeness is NOT asserted`;
14201
14620
  }
14202
14621
  /** Undelivered-mentions partial result (send) or the pending list. */
14203
14622
  function formatAgentPendingMentionActions(actions, opts = {}) {
14623
+ const style = opts.hints ?? "cli";
14204
14624
  const unresolvedMentionHandles = opts.source === "send" ? Array.from(new Set(opts.unresolvedMentionHandles ?? [])) : [];
14205
- const pageNote = opts.source === "pending" ? pendingPageVerdict(opts.hasMore, opts.limit, actions.length) : "";
14625
+ const pageNote = opts.source === "pending" ? pendingPageVerdict(opts.hasMore, opts.limit, actions.length, style) : "";
14206
14626
  if (actions.length === 0 && unresolvedMentionHandles.length === 0) return opts.source === "pending" ? `Pending mention actions\n\nNo pending mention actions. (${pageNote})\n` : "";
14207
14627
  if (opts.source === "send") {
14208
14628
  const lines = [
14209
14629
  "Undelivered mentions — partial result",
14210
14630
  "Message effect: status=queued. Queue acceptance is the only message proof.",
14211
- "Do not rerun `raft message send`; the message is already queued and a retry could duplicate it.",
14631
+ `Do not rerun \`${formatHintName(RAFT_HINTS.messageSendName(), style)}\`; the message is already queued and a retry could duplicate it.`,
14212
14632
  "Each row below is bound to the literal @token from your message.",
14213
14633
  "For a literal name rather than a recipient, wrap the @handle in inline or fenced code.",
14214
14634
  ""
@@ -14222,10 +14642,10 @@ function formatAgentPendingMentionActions(actions, opts = {}) {
14222
14642
  if (action.messageId) lines.push(` message: ${action.messageId}`);
14223
14643
  lines.push(` expires: ${action.expiresAt ?? "unknown"}`);
14224
14644
  if (action.recoveryCommand) {
14225
- lines.push(` recovery: ${action.recoveryCommand}`);
14645
+ lines.push(` recovery: ${style === "cli" ? action.recoveryCommand : formatAgentMentionNotifyRecoveryCommand(action.resolutionId, style)}`);
14226
14646
  lines.push(" note: the handle resolved, but the target was not in this conversation at send time. This does not prove the person left the server.");
14227
14647
  lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
14228
- } else lines.push(" recovery: unavailable because the pending action id is invalid; inspect `raft mention pending` without resending the message.");
14648
+ } else lines.push(` recovery: unavailable because the pending action id is invalid; inspect \`${formatHint(RAFT_HINTS.mentionPending(), style)}\` without resending the message.`);
14229
14649
  }
14230
14650
  for (const rawHandle of unresolvedMentionHandles) {
14231
14651
  const warning = toAgentSenderUnresolvedMentionWarning(rawHandle);
@@ -14249,11 +14669,11 @@ function formatAgentPendingMentionActions(actions, opts = {}) {
14249
14669
  if (action.messageId) lines.push(` message: ${action.messageId}`);
14250
14670
  lines.push(` reason: ${formatPendingReason(action.reason)}`);
14251
14671
  if (action.expiresAt) lines.push(` expires: ${action.expiresAt}`);
14252
- const commands = formatActionCommands(action);
14672
+ const commands = formatActionCommands(action, style);
14253
14673
  if (commands.length > 0) {
14254
14674
  lines.push(" recovery commands:");
14255
14675
  lines.push(...commands);
14256
- if (commands.some((command) => command.includes(" mention notify "))) lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
14676
+ if (actionVerbs(action).includes("notify")) lines.push(" note: notify exits nonzero unless the target queue accepts the delivery.");
14257
14677
  }
14258
14678
  }
14259
14679
  return `${lines.join("\n")}\n`;
@@ -14309,7 +14729,7 @@ const executeMentionActionRequestSchema = requestSchema()(object({
14309
14729
  resolutionIds: array(string()).describe("resolutionId values from mentions.pending.")
14310
14730
  }));
14311
14731
  const senderMentionDeliveriesRequestSchema = requestSchema()(object({ messageId: string().describe("Id of a message you sent.") }));
14312
- async function pendingMentionActions(client, request = {}) {
14732
+ async function pendingMentionActions(client, request = {}, options = {}) {
14313
14733
  const invalid = validateOpRequest(pendingMentionActionsRequestSchema, request);
14314
14734
  if (invalid) return invalid;
14315
14735
  const result = await client.mentions.pendingActions(request.limit === void 0 ? {} : { limit: String(request.limit) });
@@ -14317,15 +14737,10 @@ async function pendingMentionActions(client, request = {}) {
14317
14737
  const actions = normalizeAgentPendingMentionActions(result.data);
14318
14738
  const hasMore = typeof result.data.has_more === "boolean" ? result.data.has_more : null;
14319
14739
  const first = actions.find((a) => a.availableActions.length > 0);
14320
- const next = first ? {
14321
- kind: "resolve_mention",
14322
- command: `raft mention notify ${first.resolutionId}`,
14323
- args: {
14324
- action: "notify",
14325
- resolutionIds: [first.resolutionId]
14326
- },
14327
- why: "This @mention reached nobody at send time; notify the target (or add them) so the message is seen."
14328
- } : null;
14740
+ const next = first ? hintStep("resolve_mention", RAFT_HINTS.mentionAction("notify", first.resolutionId), "This @mention reached nobody at send time; notify the target (or add them) so the message is seen.", options.hints, {
14741
+ action: "notify",
14742
+ resolutionIds: [first.resolutionId]
14743
+ }) : null;
14329
14744
  return {
14330
14745
  ok: true,
14331
14746
  state: actions.length > 0 ? "pending" : "empty",
@@ -14338,7 +14753,8 @@ async function pendingMentionActions(client, request = {}) {
14338
14753
  text: formatAgentPendingMentionActions(actions, {
14339
14754
  source: "pending",
14340
14755
  ...hasMore === null ? {} : { hasMore },
14341
- limit: request.limit
14756
+ limit: request.limit,
14757
+ hints: options.hints
14342
14758
  })
14343
14759
  };
14344
14760
  }
@@ -14436,7 +14852,7 @@ async function getManualTopic(client, request) {
14436
14852
  text: formatAgentKnowledgeStdout(result.data.content)
14437
14853
  };
14438
14854
  }
14439
- async function searchManual(client, request) {
14855
+ async function searchManual(client, request, options = {}) {
14440
14856
  const malformed = validateOpRequest(searchManualRequestSchema, request);
14441
14857
  if (malformed) return malformed;
14442
14858
  const invalid = requireContext(request);
@@ -14452,12 +14868,7 @@ async function searchManual(client, request) {
14452
14868
  const data = result.data;
14453
14869
  const results = data.results;
14454
14870
  const first = results[0];
14455
- const next = first ? {
14456
- kind: "read_manual_topic",
14457
- command: `raft manual get ${first.slug} --intent "…" --reason "…"`,
14458
- args: { topic: first.slug },
14459
- why: "Open the best-matching topic."
14460
- } : null;
14871
+ const next = first ? hintStep("read_manual_topic", RAFT_HINTS.manualGet(first.slug), "Open the best-matching topic.", options.hints, { topic: first.slug }) : null;
14461
14872
  return {
14462
14873
  ok: true,
14463
14874
  state: results.length > 0 ? "results" : "empty",
@@ -14658,43 +15069,45 @@ function formatAgentActionCardPosted(target, messageId) {
14658
15069
  * the preparer in the card's own thread; a card posted inside a thread has no
14659
15070
  * thread of its own, so the reply lands in that same thread.
14660
15071
  */
14661
- function awaitConfirmationNext(target, messageId) {
15072
+ function awaitConfirmationNext(target, messageId, style) {
14662
15073
  const short = messageId.slice(0, 8);
14663
- if (getParentTargetForThread(target) !== null) return {
14664
- kind: "await_confirmation",
14665
- command: `raft message read --target "${target}" --around ${short}`,
14666
- args: {
14667
- target,
14668
- messageId
14669
- },
14670
- why: "A human must click the card to commit it. When it is executed (or fails), the outcome arrives as a reply in this thread that @mentions you; you do not need to poll."
14671
- };
15074
+ if (getParentTargetForThread(target) !== null) return hintStep("await_confirmation", RAFT_HINTS.messageRead({
15075
+ target,
15076
+ around: {
15077
+ shown: short,
15078
+ id: messageId
15079
+ }
15080
+ }), "A human must click the card to commit it. When it is executed (or fails), the outcome arrives as a reply in this thread that @mentions you; you do not need to poll.", style, {
15081
+ target,
15082
+ around: messageId,
15083
+ messageId
15084
+ });
14672
15085
  const thread = agentTaskThreadTarget(target, messageId);
14673
- return {
14674
- kind: "await_confirmation",
14675
- command: `raft message read --target "${thread}"`,
14676
- args: {
14677
- target: thread,
14678
- messageId
14679
- },
14680
- why: "A human must click the card to commit it. When it is executed (or fails), the outcome arrives as a reply in the card's thread that @mentions you; you do not need to poll."
14681
- };
15086
+ return hintStep("await_confirmation", RAFT_HINTS.messageRead({ target: thread }), "A human must click the card to commit it. When it is executed (or fails), the outcome arrives as a reply in the card's thread that @mentions you; you do not need to poll.", style, {
15087
+ target: thread,
15088
+ messageId
15089
+ });
14682
15090
  }
14683
- async function prepareActionCard(client, request) {
15091
+ async function prepareActionCard(client, request, options = {}) {
14684
15092
  if (typeof request?.target !== "string" || !request.target.trim() || !request.action) return failureOutcome(opError("INVALID_REQUEST", { message: "A target and an action are required to prepare a card." }));
14685
15093
  const invalid = validateOpRequest(prepareActionCardRequestSchema, request);
14686
15094
  if (invalid) return invalid;
14687
- const result = await client.actions.prepare(request);
14688
- if (!result.ok) return failureFromClientResult(result);
15095
+ const idempotencyKey = request.idempotencyKey?.trim() || globalThis.crypto.randomUUID();
15096
+ const result = await client.actions.prepare({
15097
+ ...request,
15098
+ idempotencyKey
15099
+ });
15100
+ if (!result.ok) return keyedWriteFailure(failureFromClientResult(result), idempotencyKey);
14689
15101
  const card = {
14690
15102
  target: request.target,
14691
- messageId: result.data.messageId
15103
+ messageId: result.data.messageId,
15104
+ idempotencyKey
14692
15105
  };
14693
15106
  return {
14694
15107
  ok: true,
14695
15108
  state: "prepared",
14696
15109
  data: card,
14697
- next: awaitConfirmationNext(request.target, card.messageId),
15110
+ next: awaitConfirmationNext(request.target, card.messageId, options.hints ?? "cli"),
14698
15111
  text: formatAgentActionCardPosted(request.target, card.messageId)
14699
15112
  };
14700
15113
  }
@@ -15105,6 +15518,14 @@ const OPERATION_DEFS = [
15105
15518
  consumes: nothing,
15106
15519
  output: small
15107
15520
  },
15521
+ {
15522
+ name: "attachments.downloadUrl",
15523
+ description: "Get a short-lived (5 minute) URL for an attachment's bytes, with its filename and MIME type, for runtimes that fetch files themselves. The URL grants access to the file: do not post it in messages.",
15524
+ schema: downloadAttachmentUrlRequestSchema,
15525
+ routes: ["attachmentDownloadUrl"],
15526
+ consumes: nothing,
15527
+ output: small
15528
+ },
15108
15529
  {
15109
15530
  name: "attachments.comments",
15110
15531
  description: "List the comments on an attachment.",
@@ -15149,9 +15570,11 @@ const OPERATION_DEFS = [
15149
15570
  schema: prepareActionCardRequestSchema,
15150
15571
  fieldDescriptions: {
15151
15572
  target: "Conversation to post the card in: `#channel`, `dm:@peer`, or a thread.",
15152
- action: "The operation the card proposes: `type` picks it, and the other fields apply per type as described."
15573
+ action: "The operation the card proposes: `type` picks it, and the other fields apply per type as described.",
15574
+ idempotencyKey: "One key per logical prepare; generated when omitted and returned. Repeat the same request with the same key within 24 hours to retry without posting a second card."
15153
15575
  },
15154
15576
  routes: ["actionPrepare"],
15577
+ idempotencyArg: "idempotencyKey",
15155
15578
  consumes: nothing,
15156
15579
  output: small
15157
15580
  },
@@ -15202,6 +15625,7 @@ const OPERATION_DEFS = [
15202
15625
  description: "Create one or more tasks on a channel's board; each gets its own thread.",
15203
15626
  schema: createTasksRequestSchema,
15204
15627
  routes: ["taskCreate"],
15628
+ idempotencyArg: "idempotencyKey",
15205
15629
  consumes: nothing,
15206
15630
  output: small
15207
15631
  },
@@ -15403,10 +15827,6 @@ const OPERATION_DEFS = [
15403
15827
  output: small
15404
15828
  }
15405
15829
  ];
15406
- /** `tasks.updateStatus` → `tasks_update_status`. */
15407
- function raftToolNameFor(name) {
15408
- return name.replace(/\./g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
15409
- }
15410
15830
  const IDEMPOTENCY_STRENGTH = {
15411
15831
  natural: 0,
15412
15832
  key: 1,
@@ -15478,11 +15898,17 @@ function createRaft(options) {
15478
15898
  const serverUrl = requireServerUrl(options.serverUrl);
15479
15899
  const authorization = `Bearer ${requireAgentCredential(options.credential)}`;
15480
15900
  const frontier = SeenFrontier.fromSnapshot(options.frontier);
15901
+ if (options.hints !== void 0 && options.hints !== "cli" && options.hints !== "tool") throw new TypeError("createRaft: hints must be \"cli\" or \"tool\".");
15902
+ const hints = options.hints ?? "cli";
15903
+ /** How every operation renders its hints. */
15904
+ const style = { hints };
15905
+ /** The SDK's default NOT_FOUND next action names a command; render it in the configured style. */
15906
+ const styled = (outcome) => restyleDefaultNextAction(outcome, hints);
15481
15907
  const session = new RaftStateSession(options.state, frontier, options.onStateSaveError);
15482
15908
  /** Load before, run, then save if the operation succeeded and changed the state. */
15483
15909
  const withState = async (run) => {
15484
15910
  await session.ensureLoaded();
15485
- const outcome = await run();
15911
+ const outcome = styled(await run());
15486
15912
  if (outcome.ok) await session.save();
15487
15913
  return outcome;
15488
15914
  };
@@ -15518,7 +15944,7 @@ function createRaft(options) {
15518
15944
  const outcome = await sendMessage(api, pending ? {
15519
15945
  ...request,
15520
15946
  idempotencyKey: pending.idempotencyKey
15521
- } : request, seen);
15947
+ } : request, seen, style);
15522
15948
  if (outcome.ok && outcome.state === "interrupted" && outcome.interrupt.resume.idempotencyKey) session.rememberContinuation({
15523
15949
  target: request.target,
15524
15950
  idempotencyKey: outcome.interrupt.resume.idempotencyKey,
@@ -15533,7 +15959,7 @@ function createRaft(options) {
15533
15959
  const outcome = await checkInbox(inboxApi, {
15534
15960
  ...request,
15535
15961
  ...since === void 0 ? {} : { since }
15536
- }, seen);
15962
+ }, seen, style);
15537
15963
  if (outcome.ok) {
15538
15964
  session.markDirty();
15539
15965
  if (since !== void 0 && (request.ack ?? "cursor") === "cursor") session.commit(since);
@@ -15552,7 +15978,7 @@ function createRaft(options) {
15552
15978
  if (batch.ackMode === "cursor" && batch.messages.length > 0) session.setPending(batch.cursor);
15553
15979
  session.markDirty();
15554
15980
  await session.save();
15555
- });
15981
+ }, style);
15556
15982
  })();
15557
15983
  const commitInbox = async (target) => {
15558
15984
  await session.ensureLoaded();
@@ -15564,7 +15990,7 @@ function createRaft(options) {
15564
15990
  };
15565
15991
  /** `seen` undefined: read without recording anything (a code read). */
15566
15992
  const readWithState = (request, seen) => withState(async () => {
15567
- const outcome = await readHistory(api, request, seen);
15993
+ const outcome = await readHistory(api, request, seen, style);
15568
15994
  if (outcome.ok && seen) session.markDirty();
15569
15995
  return outcome;
15570
15996
  });
@@ -15573,16 +15999,16 @@ function createRaft(options) {
15573
15999
  wake: {
15574
16000
  verifyNotice: (input) => verifyInboxNotice(input),
15575
16001
  webhook: {
15576
- status: () => webhookStatus(api),
15577
- register: (request) => registerWebhook(api, request),
15578
- unregister: () => unregisterWebhook(api)
16002
+ status: () => webhookStatus(api).then(styled),
16003
+ register: (request) => registerWebhook(api, request).then(styled),
16004
+ unregister: () => unregisterWebhook(api).then(styled)
15579
16005
  }
15580
16006
  },
15581
16007
  inbox: {
15582
16008
  check: (request = {}) => checkWithState(request, frontier),
15583
16009
  commit: (target) => commitInbox(target),
15584
16010
  drain: (request = {}) => drainWithState(request, frontier),
15585
- list: (request) => listInbox(api, request)
16011
+ list: (request) => listInbox(api, request, style).then(styled)
15586
16012
  },
15587
16013
  messages: {
15588
16014
  read: (request) => readWithState(request, frontier),
@@ -15591,10 +16017,10 @@ function createRaft(options) {
15591
16017
  ...request,
15592
16018
  target: message.target
15593
16019
  }, frontier),
15594
- search: (request) => searchMessages(api, request),
15595
- resolve: (request) => resolveMessage(api, request),
15596
- react: (request) => reactToMessage(api, request, "add"),
15597
- unreact: (request) => reactToMessage(api, request, "remove")
16020
+ search: (request) => searchMessages(api, request, style).then(styled),
16021
+ resolve: (request) => resolveMessage(api, request, style).then(styled),
16022
+ react: (request) => reactToMessage(api, request, "add").then(styled),
16023
+ unreact: (request) => reactToMessage(api, request, "remove").then(styled)
15598
16024
  },
15599
16025
  attachments: {
15600
16026
  upload: (request) => uploadAttachment(api, {
@@ -15602,51 +16028,52 @@ function createRaft(options) {
15602
16028
  fetch: options.fetch ?? fetch,
15603
16029
  headers: Object.fromEntries(new Headers(options.headers)),
15604
16030
  authorization
15605
- }, request),
15606
- download: (request) => downloadAttachment(api, request),
15607
- comments: (request) => attachmentComments(api, request)
16031
+ }, request, style).then(styled),
16032
+ download: (request) => downloadAttachment(api, request).then(styled),
16033
+ downloadUrl: (request) => downloadAttachmentUrl(api, request, style).then(styled),
16034
+ comments: (request) => attachmentComments(api, request).then(styled)
15608
16035
  },
15609
16036
  mentions: {
15610
- pending: (request) => pendingMentionActions(api, request),
15611
- execute: (request) => executeMentionAction(api, request),
15612
- deliveries: (request) => senderMentionDeliveries(api, request)
16037
+ pending: (request) => pendingMentionActions(api, request, style).then(styled),
16038
+ execute: (request) => executeMentionAction(api, request).then(styled),
16039
+ deliveries: (request) => senderMentionDeliveries(api, request).then(styled)
15613
16040
  },
15614
- actions: { prepare: (request) => prepareActionCard(api, request) },
16041
+ actions: { prepare: (request) => prepareActionCard(api, request, style).then(styled) },
15615
16042
  manual: {
15616
- get: (request) => getManualTopic(api, request),
15617
- search: (request) => searchManual(api, request)
16043
+ get: (request) => getManualTopic(api, request).then(styled),
16044
+ search: (request) => searchManual(api, request, style).then(styled)
15618
16045
  },
15619
16046
  tasks: {
15620
- claim: (request) => claimTasks(api, request),
15621
- list: (request) => listTasks(api, request),
15622
- create: (request) => createTasks(api, request),
15623
- unclaim: (request) => unclaimTask(api, request),
15624
- assign: (request) => assignTask(api, request),
15625
- unassign: (request) => unassignTask(api, request),
15626
- updateStatus: (request) => updateTaskStatus(api, request),
15627
- amend: (request) => amendTask(api, request),
15628
- history: (request) => taskHistory(api, request),
15629
- show: (request) => showTask(api, request),
15630
- convert: (request) => convertMessageToTask(api, request),
15631
- delete: (request) => deleteTask(api, request)
16047
+ claim: (request) => claimTasks(api, request, style).then(styled),
16048
+ list: (request) => listTasks(api, request, style).then(styled),
16049
+ create: (request) => createTasks(api, request, style).then(styled),
16050
+ unclaim: (request) => unclaimTask(api, request).then(styled),
16051
+ assign: (request) => assignTask(api, request).then(styled),
16052
+ unassign: (request) => unassignTask(api, request).then(styled),
16053
+ updateStatus: (request) => updateTaskStatus(api, request, style).then(styled),
16054
+ amend: (request) => amendTask(api, request, style).then(styled),
16055
+ history: (request) => taskHistory(api, request).then(styled),
16056
+ show: (request) => showTask(api, request, style).then(styled),
16057
+ convert: (request) => convertMessageToTask(api, request, style).then(styled),
16058
+ delete: (request) => deleteTask(api, request).then(styled)
15632
16059
  },
15633
16060
  channels: {
15634
- join: (request) => joinChannel(api, request),
15635
- leave: (request) => leaveChannel(api, request),
15636
- mute: (request) => muteChannel(api, request),
15637
- unmute: (request) => unmuteChannel(api, request),
15638
- members: (request) => channelMembers(api, request),
15639
- info: (request) => channelInfo(api, request)
16061
+ join: (request) => joinChannel(api, request, style).then(styled),
16062
+ leave: (request) => leaveChannel(api, request, style).then(styled),
16063
+ mute: (request) => muteChannel(api, request, style).then(styled),
16064
+ unmute: (request) => unmuteChannel(api, request, style).then(styled),
16065
+ members: (request) => channelMembers(api, request).then(styled),
16066
+ info: (request) => channelInfo(api, request, style).then(styled)
15640
16067
  },
15641
16068
  threads: {
15642
- list: () => listThreads(api),
15643
- unfollow: (request) => unfollowThread(api, request)
16069
+ list: () => listThreads(api).then(styled),
16070
+ unfollow: (request) => unfollowThread(api, request).then(styled)
15644
16071
  },
15645
- server: { info: (request) => serverInfo(api, request) },
15646
- users: { info: (request) => userInfo(api, request) },
16072
+ server: { info: (request) => serverInfo(api, request, style).then(styled) },
16073
+ users: { info: (request) => userInfo(api, request, style).then(styled) },
15647
16074
  profile: {
15648
- show: (request) => showProfile(api, request),
15649
- update: (request) => updateProfile(api, request)
16075
+ show: (request) => showProfile(api, request).then(styled),
16076
+ update: (request) => updateProfile(api, request).then(styled)
15650
16077
  },
15651
16078
  frontier,
15652
16079
  state: {
@@ -15667,7 +16094,7 @@ function createRaft(options) {
15667
16094
  const drain = drainWithState(args, call.seen);
15668
16095
  const batches = [];
15669
16096
  for (let step = await drain.next();; step = await drain.next()) {
15670
- if (step.done) return drainedInboxOutcome(batches, step.value);
16097
+ if (step.done) return styled(drainedInboxOutcome(batches, step.value, style));
15671
16098
  batches.push(step.value);
15672
16099
  }
15673
16100
  },
@@ -15695,6 +16122,7 @@ function createRaft(options) {
15695
16122
  "messages.resolve": (args) => raft.messages.resolve(args),
15696
16123
  "messages.react": (args) => raft.messages.react(args),
15697
16124
  "messages.unreact": (args) => raft.messages.unreact(args),
16125
+ "attachments.downloadUrl": (args) => raft.attachments.downloadUrl(args),
15698
16126
  "attachments.comments": (args) => raft.attachments.comments(args),
15699
16127
  "mentions.pending": (args) => raft.mentions.pending(args),
15700
16128
  "mentions.execute": (args) => raft.mentions.execute(args),
@@ -15798,6 +16226,7 @@ exports.createRaftClientFromStore = createRaftClientFromStore;
15798
16226
  exports.createRaftRoutes = createRaftRoutes;
15799
16227
  exports.createTasksRequestSchema = createTasksRequestSchema;
15800
16228
  exports.describeRaftRoute = describeRaftRoute;
16229
+ exports.downloadAttachmentUrlRequestSchema = downloadAttachmentUrlRequestSchema;
15801
16230
  exports.drainInboxRequestSchema = drainInboxRequestSchema;
15802
16231
  exports.executeMentionActionRequestSchema = executeMentionActionRequestSchema;
15803
16232
  exports.getManualTopicRequestSchema = getManualTopicRequestSchema;