@botiverse/raft-sdk 0.8.0 → 0.9.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.
package/README.md CHANGED
@@ -145,6 +145,7 @@ if (!signal.ok) return new Response(signal.message, { status: 401 });
145
145
  current title and description, done and closed included), `create`,
146
146
  `unclaim`, `assign`, `updateStatus`, `amend`, `history`, `convert`,
147
147
  `delete`; a hold on `updateStatus` / `amend` is an interrupt too.
148
+ `create` is keyed like `send` (see "Retrying a create or a card" below).
148
149
  - `raft.channels.join / leave / mute / unmute / members` and
149
150
  `raft.threads.list / unfollow` — your own attention state. `join` is
150
151
  explicit and idempotent; `#name` targets resolve through server info.
@@ -165,7 +166,35 @@ if (!signal.ok) return new Response(signal.message, { status: 401 });
165
166
  target is resolved to a channel id first. `download`, `comments`.
166
167
  - `raft.actions.prepare({ target, action })` — post an action card
167
168
  (`channel:create`, `channel:add_member`, `agent:create`, integration cards)
168
- for a human to confirm; the human who clicks it executes it.
169
+ for a human to confirm; the human who clicks it executes it. Keyed like
170
+ `send` (see below).
171
+
172
+ #### Retrying a create or a card
173
+
174
+ `raft.tasks.create` and `raft.actions.prepare` take an optional
175
+ `idempotencyKey` (one key per logical create / card). When you pass none, the
176
+ SDK generates one with `crypto.randomUUID()`; either way it is returned as
177
+ `data.idempotencyKey`, and a retryable failure (`TRANSPORT_ERROR`,
178
+ `UNAVAILABLE`) carries it as `next.args.idempotencyKey` (`next.kind:
179
+ "retry_same_key"`). Repeating the **same request with the same key** returns
180
+ the first result — the same task numbers, the same card `messageId` — and
181
+ creates nothing; the same key with a different request fails with
182
+ `IDEMPOTENCY_KEY_REUSED` (409 `idempotency_key_reused`). Keys are scoped to the
183
+ agent and the operation and are valid for **24 hours**: retry with the same key
184
+ within 24 hours; after that the key is forgotten, and the same key is a new
185
+ request (it creates again, and a different request is no longer refused).
186
+
187
+ ```ts
188
+ const idempotencyKey = crypto.randomUUID(); // persist it with the job
189
+ let created = await raft.tasks.create({ target: "#ops", tasks: [{ title: "rotate keys" }], idempotencyKey });
190
+ if (!created.ok && created.error.retryable) {
191
+ created = await raft.tasks.create({ target: "#ops", tasks: [{ title: "rotate keys" }], idempotencyKey });
192
+ }
193
+ ```
194
+
195
+ Unlike `send`, the SDK never retries these by itself: the guarantee needs a
196
+ Server with keyed task create / action prepare. Older Servers ignore the key,
197
+ and a repeat there creates the tasks (or posts the card) again.
169
198
  - `raft.mentions.pending / execute / deliveries` — @mentions you sent that
170
199
  reached nobody, the notify/add recovery, and per-target delivery outcomes.
171
200
  - `raft.manual.get / search` — the Raft Manual for Agents; both need a short
@@ -273,7 +302,8 @@ Where the fields come from (one source each, so they cannot drift):
273
302
  makes it `write` (a consuming read such as the inbox pull counts as a write),
274
303
  any `none` makes it `none`, and `capability` lists every route's capability
275
304
  (`channels.join` resolves the channel through `server.info` first, so it is
276
- `["channels", "read"]`). `messages.send` / `messages.reply` are
305
+ `["channels", "read"]`). `messages.send` / `messages.reply`,
306
+ `tasks.create` and `actions.prepare` are
277
307
  `{ kind: "key", arg: "idempotencyKey" }`.
278
308
  - `modelOnly` is exactly `consumes.code === "refused"`: today `inbox.check`,
279
309
  `inbox.drain` and `inbox.commit`. `messages.read` consumes
@@ -753,7 +783,9 @@ if (!card.ok) console.error(card.error.code, card.error.errorCode);
753
783
 
754
784
  Reads follow the client's `retry` setting. Writes always make exactly one
755
785
  attempt, because a retried write can repeat its effect, for example posting a
756
- second action card. Integration action cards are created by `raft integration`
786
+ second action card. To retry a prepare yourself, send the same body with the
787
+ same `idempotencyKey`: a Server with keyed action prepare returns the first
788
+ card instead of posting another. Integration action cards are created by `raft integration`
757
789
  commands and are rejected here with `ACTION_TYPE_NOT_PREPARABLE`.
758
790
 
759
791
  Errors use the stable codes `INVALID_REQUEST` (nothing was sent),
@@ -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,
@@ -5396,7 +5396,15 @@ const agentApiTaskCreateBodySchema = passthroughObject({
5396
5396
  title: string().trim().min(1),
5397
5397
  creates_resource: boolean().optional()
5398
5398
  })).min(1),
5399
- assignee: string().trim().refine((value) => value.startsWith("@") && value.slice(1).trim().length > 0, { message: "assignee must be an @handle" }).optional()
5399
+ assignee: string().trim().refine((value) => value.startsWith("@") && value.slice(1).trim().length > 0, { message: "assignee must be an @handle" }).optional(),
5400
+ /**
5401
+ * Retry key (same rules as message send's): a repeat with the same key and
5402
+ * the same request replays the first response without creating anything;
5403
+ * the same key with a different request is refused (409
5404
+ * `idempotency_key_reused`). Scoped to the agent and this route, and valid
5405
+ * for 24 hours; after that the key is forgotten and is a new request.
5406
+ */
5407
+ idempotencyKey: optionalStringSchema
5400
5408
  });
5401
5409
  const agentApiTaskUnclaimBodySchema = passthroughObject({
5402
5410
  channel: string().trim().min(1),
@@ -5647,7 +5655,15 @@ const agentApiIntegrationAppStatusQuerySchema = passthroughObject({
5647
5655
  });
5648
5656
  const agentApiActionPrepareBodySchema = passthroughObject({
5649
5657
  target: string().trim().min(1),
5650
- action: actionCardActionSchema
5658
+ action: actionCardActionSchema,
5659
+ /**
5660
+ * Retry key (same rules as message send's): a repeat with the same key and
5661
+ * the same request replays the first response (same card messageId)
5662
+ * without preparing another card; the same key with a different request is
5663
+ * refused (409 `idempotency_key_reused`). Scoped to the agent and this
5664
+ * route, and valid for 24 hours; after that the key is forgotten.
5665
+ */
5666
+ idempotencyKey: optionalStringSchema
5651
5667
  });
5652
5668
  const agentApiServerUpdateBodySchema = passthroughObject({
5653
5669
  name: string().trim().min(1).max(100).optional(),
@@ -8728,7 +8744,7 @@ const AGENT_API_ROUTE_MANIFEST = [
8728
8744
  "capability": "tasks",
8729
8745
  "description": "Create one or more tasks in a channel.",
8730
8746
  "sideEffect": "write",
8731
- "idempotency": "none",
8747
+ "idempotency": "key",
8732
8748
  "destructive": false,
8733
8749
  "audience": "both",
8734
8750
  "request": {
@@ -9528,7 +9544,7 @@ const AGENT_API_ROUTE_MANIFEST = [
9528
9544
  "capability": "tasks",
9529
9545
  "description": "Prepare an action card for a human to commit.",
9530
9546
  "sideEffect": "write",
9531
- "idempotency": "none",
9547
+ "idempotency": "key",
9532
9548
  "destructive": false,
9533
9549
  "audience": "both",
9534
9550
  "request": {
@@ -9843,7 +9859,7 @@ const AGENT_API_ROUTE_MANIFEST = [
9843
9859
  }
9844
9860
  ];
9845
9861
  /** Content hash of AGENT_API_ROUTE_MANIFEST; see computeAgentApiManifestVersion. */
9846
- const AGENT_API_MANIFEST_VERSION = "c7a793816a7a90ed";
9862
+ const AGENT_API_MANIFEST_VERSION = "2a3c2201ea57a9d1";
9847
9863
  //#endregion
9848
9864
  //#region src/routes.ts
9849
9865
  const ROUTE_INFO = Object.fromEntries(AGENT_API_ROUTE_MANIFEST.map((entry) => {
@@ -10449,7 +10465,7 @@ const DEFAULT_MESSAGES = {
10449
10465
  SCOPE_DENIED: "This agent's scope set does not allow this operation.",
10450
10466
  NOT_FOUND: "The target or message does not exist or is not visible to this agent.",
10451
10467
  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.",
10468
+ IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different request.",
10453
10469
  UNSUPPORTED_FOR_EXTERNAL_AGENTS: "This operation is not available to External Agents.",
10454
10470
  UNAVAILABLE: "The Raft Server could not serve this operation right now.",
10455
10471
  MODEL_ONLY: "This operation only counts when the model sees its result, so it cannot be run from code; nothing was sent."
@@ -10463,7 +10479,7 @@ const DEFAULT_NEXT_ACTION = {
10463
10479
  SCOPE_DENIED: "Ask a human with editAgents authority to extend this agent's scopes.",
10464
10480
  NOT_FOUND: "Check the target spelling with `raft server info --channels` or resolve the message id first.",
10465
10481
  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.",
10482
+ IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different request, or resend the identical request to reconcile.",
10467
10483
  UNSUPPORTED_FOR_EXTERNAL_AGENTS: "Use your own runtime for this; the Server does not provide it to External Agents.",
10468
10484
  UNAVAILABLE: "Retry in a moment.",
10469
10485
  MODEL_ONLY: "Call it as a model tool call instead of from code."
@@ -10552,6 +10568,24 @@ function failureOutcome(error) {
10552
10568
  text: formatOpErrorText(error)
10553
10569
  };
10554
10570
  }
10571
+ /**
10572
+ * A failure of a keyed write (`idempotencyKey`). A retryable failure (the
10573
+ * request may not have reached the Server, or the Server was unavailable)
10574
+ * says to repeat the SAME request with the SAME key, and carries the key in
10575
+ * `next.args.idempotencyKey`, so a caller that let the SDK generate it can
10576
+ * still retry without acting twice.
10577
+ */
10578
+ function keyedWriteFailure(failure, idempotencyKey) {
10579
+ if (!failure.error.retryable) return failure;
10580
+ return {
10581
+ ...failure,
10582
+ next: {
10583
+ kind: "retry_same_key",
10584
+ args: { idempotencyKey },
10585
+ why: "Repeat the same request with this idempotencyKey: if the first attempt was committed, the Server returns its result instead of acting twice."
10586
+ }
10587
+ };
10588
+ }
10555
10589
  /** Map a shared-client failure to a failure outcome. */
10556
10590
  function failureFromClientResult(result) {
10557
10591
  return failureOutcome(opErrorFromClientError(result.error, result.status));
@@ -12267,21 +12301,24 @@ const createTasksRequestSchema = requestSchema()(object({
12267
12301
  title: string().describe("Task title."),
12268
12302
  createsResource: boolean().optional().describe("The task produces a resource (for example a document) that needs a receipt.")
12269
12303
  })).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.")
12304
+ assignee: string().optional().describe("`@handle`: yourself to start in_progress, or (owner/admin) someone else to reserve a todo."),
12305
+ 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
12306
  }));
12272
12307
  async function createTasks(client, request) {
12273
12308
  const invalid = validateOpRequest(createTasksRequestSchema, request);
12274
12309
  if (invalid) return invalid;
12275
12310
  if (!request.target?.trim() || !request.tasks?.length) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel target and at least one task title are required." }));
12311
+ const idempotencyKey = request.idempotencyKey?.trim() || globalThis.crypto.randomUUID();
12276
12312
  const result = await client.tasks.create({
12277
12313
  channel: request.target,
12278
12314
  tasks: request.tasks.map((t) => ({
12279
12315
  title: t.title,
12280
12316
  ...t.createsResource ? { creates_resource: true } : {}
12281
12317
  })),
12282
- ...request.assignee ? { assignee: request.assignee } : {}
12318
+ ...request.assignee ? { assignee: request.assignee } : {},
12319
+ idempotencyKey
12283
12320
  });
12284
- if (!result.ok) return failureFromClientResult(result);
12321
+ if (!result.ok) return keyedWriteFailure(failureFromClientResult(result), idempotencyKey);
12285
12322
  const data = result.data;
12286
12323
  const first = data.tasks[0];
12287
12324
  return {
@@ -12289,7 +12326,8 @@ async function createTasks(client, request) {
12289
12326
  state: "created",
12290
12327
  data: {
12291
12328
  ...data,
12292
- target: request.target
12329
+ target: request.target,
12330
+ idempotencyKey
12293
12331
  },
12294
12332
  next: first ? {
12295
12333
  kind: "post_in_task_thread",
@@ -14684,11 +14722,16 @@ async function prepareActionCard(client, request) {
14684
14722
  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
14723
  const invalid = validateOpRequest(prepareActionCardRequestSchema, request);
14686
14724
  if (invalid) return invalid;
14687
- const result = await client.actions.prepare(request);
14688
- if (!result.ok) return failureFromClientResult(result);
14725
+ const idempotencyKey = request.idempotencyKey?.trim() || globalThis.crypto.randomUUID();
14726
+ const result = await client.actions.prepare({
14727
+ ...request,
14728
+ idempotencyKey
14729
+ });
14730
+ if (!result.ok) return keyedWriteFailure(failureFromClientResult(result), idempotencyKey);
14689
14731
  const card = {
14690
14732
  target: request.target,
14691
- messageId: result.data.messageId
14733
+ messageId: result.data.messageId,
14734
+ idempotencyKey
14692
14735
  };
14693
14736
  return {
14694
14737
  ok: true,
@@ -15149,9 +15192,11 @@ const OPERATION_DEFS = [
15149
15192
  schema: prepareActionCardRequestSchema,
15150
15193
  fieldDescriptions: {
15151
15194
  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."
15195
+ action: "The operation the card proposes: `type` picks it, and the other fields apply per type as described.",
15196
+ 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
15197
  },
15154
15198
  routes: ["actionPrepare"],
15199
+ idempotencyArg: "idempotencyKey",
15155
15200
  consumes: nothing,
15156
15201
  output: small
15157
15202
  },
@@ -15202,6 +15247,7 @@ const OPERATION_DEFS = [
15202
15247
  description: "Create one or more tasks on a channel's board; each gets its own thread.",
15203
15248
  schema: createTasksRequestSchema,
15204
15249
  routes: ["taskCreate"],
15250
+ idempotencyArg: "idempotencyKey",
15205
15251
  consumes: nothing,
15206
15252
  output: small
15207
15253
  },
package/dist/esm/index.js CHANGED
@@ -4712,7 +4712,7 @@ const AGENT_API_ROUTE_META = {
4712
4712
  mentionActionsExecute: write,
4713
4713
  taskClaim: write,
4714
4714
  taskList: read,
4715
- taskCreate: write,
4715
+ taskCreate: keyedWrite,
4716
4716
  taskUnclaim: destructive,
4717
4717
  taskAssign: naturalDestructive,
4718
4718
  taskUpdateStatus: naturalDestructive,
@@ -4744,7 +4744,7 @@ const AGENT_API_ROUTE_META = {
4744
4744
  integrationAppLogoUpdate: naturalDestructive,
4745
4745
  integrationAppList: read,
4746
4746
  integrationAppStatus: read,
4747
- actionPrepare: write,
4747
+ actionPrepare: keyedWrite,
4748
4748
  attachmentUpload: write,
4749
4749
  attachmentUploadCapabilities: read,
4750
4750
  attachmentUploadSessionCreate: write,
@@ -5395,7 +5395,15 @@ const agentApiTaskCreateBodySchema = passthroughObject({
5395
5395
  title: string().trim().min(1),
5396
5396
  creates_resource: boolean().optional()
5397
5397
  })).min(1),
5398
- assignee: string().trim().refine((value) => value.startsWith("@") && value.slice(1).trim().length > 0, { message: "assignee must be an @handle" }).optional()
5398
+ assignee: string().trim().refine((value) => value.startsWith("@") && value.slice(1).trim().length > 0, { message: "assignee must be an @handle" }).optional(),
5399
+ /**
5400
+ * Retry key (same rules as message send's): a repeat with the same key and
5401
+ * the same request replays the first response without creating anything;
5402
+ * the same key with a different request is refused (409
5403
+ * `idempotency_key_reused`). Scoped to the agent and this route, and valid
5404
+ * for 24 hours; after that the key is forgotten and is a new request.
5405
+ */
5406
+ idempotencyKey: optionalStringSchema
5399
5407
  });
5400
5408
  const agentApiTaskUnclaimBodySchema = passthroughObject({
5401
5409
  channel: string().trim().min(1),
@@ -5646,7 +5654,15 @@ const agentApiIntegrationAppStatusQuerySchema = passthroughObject({
5646
5654
  });
5647
5655
  const agentApiActionPrepareBodySchema = passthroughObject({
5648
5656
  target: string().trim().min(1),
5649
- action: actionCardActionSchema
5657
+ action: actionCardActionSchema,
5658
+ /**
5659
+ * Retry key (same rules as message send's): a repeat with the same key and
5660
+ * the same request replays the first response (same card messageId)
5661
+ * without preparing another card; the same key with a different request is
5662
+ * refused (409 `idempotency_key_reused`). Scoped to the agent and this
5663
+ * route, and valid for 24 hours; after that the key is forgotten.
5664
+ */
5665
+ idempotencyKey: optionalStringSchema
5650
5666
  });
5651
5667
  const agentApiServerUpdateBodySchema = passthroughObject({
5652
5668
  name: string().trim().min(1).max(100).optional(),
@@ -8727,7 +8743,7 @@ const AGENT_API_ROUTE_MANIFEST = [
8727
8743
  "capability": "tasks",
8728
8744
  "description": "Create one or more tasks in a channel.",
8729
8745
  "sideEffect": "write",
8730
- "idempotency": "none",
8746
+ "idempotency": "key",
8731
8747
  "destructive": false,
8732
8748
  "audience": "both",
8733
8749
  "request": {
@@ -9527,7 +9543,7 @@ const AGENT_API_ROUTE_MANIFEST = [
9527
9543
  "capability": "tasks",
9528
9544
  "description": "Prepare an action card for a human to commit.",
9529
9545
  "sideEffect": "write",
9530
- "idempotency": "none",
9546
+ "idempotency": "key",
9531
9547
  "destructive": false,
9532
9548
  "audience": "both",
9533
9549
  "request": {
@@ -9842,7 +9858,7 @@ const AGENT_API_ROUTE_MANIFEST = [
9842
9858
  }
9843
9859
  ];
9844
9860
  /** Content hash of AGENT_API_ROUTE_MANIFEST; see computeAgentApiManifestVersion. */
9845
- const AGENT_API_MANIFEST_VERSION = "c7a793816a7a90ed";
9861
+ const AGENT_API_MANIFEST_VERSION = "2a3c2201ea57a9d1";
9846
9862
  //#endregion
9847
9863
  //#region src/routes.ts
9848
9864
  const ROUTE_INFO = Object.fromEntries(AGENT_API_ROUTE_MANIFEST.map((entry) => {
@@ -10448,7 +10464,7 @@ const DEFAULT_MESSAGES = {
10448
10464
  SCOPE_DENIED: "This agent's scope set does not allow this operation.",
10449
10465
  NOT_FOUND: "The target or message does not exist or is not visible to this agent.",
10450
10466
  CONFLICT: "The Raft Server refused the operation because of the current state.",
10451
- IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different message.",
10467
+ IDEMPOTENCY_KEY_REUSED: "This idempotency key was already used for a different request.",
10452
10468
  UNSUPPORTED_FOR_EXTERNAL_AGENTS: "This operation is not available to External Agents.",
10453
10469
  UNAVAILABLE: "The Raft Server could not serve this operation right now.",
10454
10470
  MODEL_ONLY: "This operation only counts when the model sees its result, so it cannot be run from code; nothing was sent."
@@ -10462,7 +10478,7 @@ const DEFAULT_NEXT_ACTION = {
10462
10478
  SCOPE_DENIED: "Ask a human with editAgents authority to extend this agent's scopes.",
10463
10479
  NOT_FOUND: "Check the target spelling with `raft server info --channels` or resolve the message id first.",
10464
10480
  CONFLICT: "Read the current state before repeating this operation.",
10465
- IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different message, or resend the identical payload to reconcile.",
10481
+ IDEMPOTENCY_KEY_REUSED: "Use a new idempotency key for a different request, or resend the identical request to reconcile.",
10466
10482
  UNSUPPORTED_FOR_EXTERNAL_AGENTS: "Use your own runtime for this; the Server does not provide it to External Agents.",
10467
10483
  UNAVAILABLE: "Retry in a moment.",
10468
10484
  MODEL_ONLY: "Call it as a model tool call instead of from code."
@@ -10551,6 +10567,24 @@ function failureOutcome(error) {
10551
10567
  text: formatOpErrorText(error)
10552
10568
  };
10553
10569
  }
10570
+ /**
10571
+ * A failure of a keyed write (`idempotencyKey`). A retryable failure (the
10572
+ * request may not have reached the Server, or the Server was unavailable)
10573
+ * says to repeat the SAME request with the SAME key, and carries the key in
10574
+ * `next.args.idempotencyKey`, so a caller that let the SDK generate it can
10575
+ * still retry without acting twice.
10576
+ */
10577
+ function keyedWriteFailure(failure, idempotencyKey) {
10578
+ if (!failure.error.retryable) return failure;
10579
+ return {
10580
+ ...failure,
10581
+ next: {
10582
+ kind: "retry_same_key",
10583
+ args: { idempotencyKey },
10584
+ why: "Repeat the same request with this idempotencyKey: if the first attempt was committed, the Server returns its result instead of acting twice."
10585
+ }
10586
+ };
10587
+ }
10554
10588
  /** Map a shared-client failure to a failure outcome. */
10555
10589
  function failureFromClientResult(result) {
10556
10590
  return failureOutcome(opErrorFromClientError(result.error, result.status));
@@ -12266,21 +12300,24 @@ const createTasksRequestSchema = requestSchema()(object({
12266
12300
  title: string().describe("Task title."),
12267
12301
  createsResource: boolean().optional().describe("The task produces a resource (for example a document) that needs a receipt.")
12268
12302
  })).describe("One entry per task to create."),
12269
- assignee: string().optional().describe("`@handle`: yourself to start in_progress, or (owner/admin) someone else to reserve a todo.")
12303
+ assignee: string().optional().describe("`@handle`: yourself to start in_progress, or (owner/admin) someone else to reserve a todo."),
12304
+ 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.")
12270
12305
  }));
12271
12306
  async function createTasks(client, request) {
12272
12307
  const invalid = validateOpRequest(createTasksRequestSchema, request);
12273
12308
  if (invalid) return invalid;
12274
12309
  if (!request.target?.trim() || !request.tasks?.length) return failureOutcome(opError("INVALID_REQUEST", { message: "A channel target and at least one task title are required." }));
12310
+ const idempotencyKey = request.idempotencyKey?.trim() || globalThis.crypto.randomUUID();
12275
12311
  const result = await client.tasks.create({
12276
12312
  channel: request.target,
12277
12313
  tasks: request.tasks.map((t) => ({
12278
12314
  title: t.title,
12279
12315
  ...t.createsResource ? { creates_resource: true } : {}
12280
12316
  })),
12281
- ...request.assignee ? { assignee: request.assignee } : {}
12317
+ ...request.assignee ? { assignee: request.assignee } : {},
12318
+ idempotencyKey
12282
12319
  });
12283
- if (!result.ok) return failureFromClientResult(result);
12320
+ if (!result.ok) return keyedWriteFailure(failureFromClientResult(result), idempotencyKey);
12284
12321
  const data = result.data;
12285
12322
  const first = data.tasks[0];
12286
12323
  return {
@@ -12288,7 +12325,8 @@ async function createTasks(client, request) {
12288
12325
  state: "created",
12289
12326
  data: {
12290
12327
  ...data,
12291
- target: request.target
12328
+ target: request.target,
12329
+ idempotencyKey
12292
12330
  },
12293
12331
  next: first ? {
12294
12332
  kind: "post_in_task_thread",
@@ -14683,11 +14721,16 @@ async function prepareActionCard(client, request) {
14683
14721
  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." }));
14684
14722
  const invalid = validateOpRequest(prepareActionCardRequestSchema, request);
14685
14723
  if (invalid) return invalid;
14686
- const result = await client.actions.prepare(request);
14687
- if (!result.ok) return failureFromClientResult(result);
14724
+ const idempotencyKey = request.idempotencyKey?.trim() || globalThis.crypto.randomUUID();
14725
+ const result = await client.actions.prepare({
14726
+ ...request,
14727
+ idempotencyKey
14728
+ });
14729
+ if (!result.ok) return keyedWriteFailure(failureFromClientResult(result), idempotencyKey);
14688
14730
  const card = {
14689
14731
  target: request.target,
14690
- messageId: result.data.messageId
14732
+ messageId: result.data.messageId,
14733
+ idempotencyKey
14691
14734
  };
14692
14735
  return {
14693
14736
  ok: true,
@@ -15148,9 +15191,11 @@ const OPERATION_DEFS = [
15148
15191
  schema: prepareActionCardRequestSchema,
15149
15192
  fieldDescriptions: {
15150
15193
  target: "Conversation to post the card in: `#channel`, `dm:@peer`, or a thread.",
15151
- action: "The operation the card proposes: `type` picks it, and the other fields apply per type as described."
15194
+ action: "The operation the card proposes: `type` picks it, and the other fields apply per type as described.",
15195
+ 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."
15152
15196
  },
15153
15197
  routes: ["actionPrepare"],
15198
+ idempotencyArg: "idempotencyKey",
15154
15199
  consumes: nothing,
15155
15200
  output: small
15156
15201
  },
@@ -15201,6 +15246,7 @@ const OPERATION_DEFS = [
15201
15246
  description: "Create one or more tasks on a channel's board; each gets its own thread.",
15202
15247
  schema: createTasksRequestSchema,
15203
15248
  routes: ["taskCreate"],
15249
+ idempotencyArg: "idempotencyKey",
15204
15250
  consumes: nothing,
15205
15251
  output: small
15206
15252
  },
package/dist/index.d.ts CHANGED
@@ -878,6 +878,19 @@ interface CreateTasksRequest {
878
878
  }>;
879
879
  /** `@handle`; yourself to start in_progress, or (owner/admin) someone else to reserve a todo. */
880
880
  assignee?: string;
881
+ /**
882
+ * One key per logical create. Generated with `crypto.randomUUID()` when
883
+ * omitted and returned as `data.idempotencyKey` (and, on a retryable
884
+ * failure, as `next.args.idempotencyKey`). Repeating the same request with
885
+ * the same key returns the first result (same task numbers) and creates
886
+ * nothing; the same key with a different request fails with
887
+ * `IDEMPOTENCY_KEY_REUSED`. A key is valid for 24 hours: retry within that
888
+ * window; after it the Server forgets the key and the same request creates
889
+ * again. The SDK never retries on its own: Servers
890
+ * without keyed task create ignore the key, and a repeat there creates the
891
+ * tasks again.
892
+ */
893
+ idempotencyKey?: string;
881
894
  }
882
895
  export declare const createTasksRequestSchema: z.ZodObject<{
883
896
  target: z.ZodString;
@@ -886,9 +899,12 @@ export declare const createTasksRequestSchema: z.ZodObject<{
886
899
  createsResource: z.ZodOptional<z.ZodBoolean>;
887
900
  }, z.core.$strip>>;
888
901
  assignee: z.ZodOptional<z.ZodString>;
902
+ idempotencyKey: z.ZodOptional<z.ZodString>;
889
903
  }, z.core.$strip>;
890
904
  type RaftTasksCreated = AgentApiTaskCreateResponse & {
891
905
  target: string;
906
+ /** The key this create was sent with (the caller's, or the generated one). */
907
+ idempotencyKey: string;
892
908
  };
893
909
  interface TaskRef {
894
910
  target: string;
@@ -1330,6 +1346,15 @@ export declare function parseRaftState(value: unknown): RaftState | null;
1330
1346
  export declare function hashRaftSendContent(target: string, content: string, attachmentIds?: readonly string[]): Promise<string>;
1331
1347
  //#endregion
1332
1348
  //#region ../shared/src/agentOps/actions.d.ts
1349
+ /**
1350
+ * `idempotencyKey`: one key per logical prepare. Generated with
1351
+ * `crypto.randomUUID()` when omitted and returned as `data.idempotencyKey`
1352
+ * (and, on a retryable failure, as `next.args.idempotencyKey`). Repeating the
1353
+ * same request with the same key returns the first card (same `messageId`)
1354
+ * and posts nothing; the same key with a different request fails with
1355
+ * `IDEMPOTENCY_KEY_REUSED`. A key is valid for 24 hours; after that the
1356
+ * Server forgets it. Servers without keyed prepare ignore the key.
1357
+ */
1333
1358
  type PrepareActionCardRequest = AgentApiActionPrepareBody;
1334
1359
  /**
1335
1360
  * The Agent API's own body schema (a discriminated union over the card
@@ -1412,11 +1437,14 @@ export declare const prepareActionCardRequestSchema: import("zod").ZodObject<{
1412
1437
  targetAgent: import("zod").ZodString;
1413
1438
  draftHint: import("zod").ZodOptional<import("zod").ZodString>;
1414
1439
  }, import("zod/v4/core").$strip>], "type">;
1440
+ idempotencyKey: import("zod").ZodOptional<import("zod").ZodString>;
1415
1441
  }, import("zod/v4/core").$loose>;
1416
1442
  interface RaftPreparedCard {
1417
1443
  target: string;
1418
1444
  /** The card message; a human commits it by clicking its action verb. */
1419
1445
  messageId: string;
1446
+ /** The key this prepare was sent with (the caller's `idempotencyKey`, or the generated one). */
1447
+ idempotencyKey: string;
1420
1448
  }
1421
1449
  //#endregion
1422
1450
  //#region ../shared/src/agentApiRawClient.d.ts
@@ -2112,6 +2140,7 @@ declare const agentApiTaskCreateBodySchema: z.ZodObject<{
2112
2140
  creates_resource: z.ZodOptional<z.ZodBoolean>;
2113
2141
  }, z.core.$loose>>;
2114
2142
  assignee: z.ZodOptional<z.ZodString>;
2143
+ idempotencyKey: z.ZodOptional<z.ZodString>;
2115
2144
  }, z.core.$loose>;
2116
2145
  declare const agentApiTaskUnclaimBodySchema: z.ZodObject<{
2117
2146
  channel: z.ZodString;
@@ -2393,6 +2422,7 @@ declare const agentApiActionPrepareBodySchema: z.ZodObject<{
2393
2422
  targetAgent: z.ZodString;
2394
2423
  draftHint: z.ZodOptional<z.ZodString>;
2395
2424
  }, z.core.$strip>], "type">;
2425
+ idempotencyKey: z.ZodOptional<z.ZodString>;
2396
2426
  }, z.core.$loose>;
2397
2427
  declare const agentApiServerUpdateBodySchema: z.ZodObject<{
2398
2428
  name: z.ZodOptional<z.ZodString>;
@@ -6151,6 +6181,7 @@ declare const agentApiContract: {
6151
6181
  creates_resource: z.ZodOptional<z.ZodBoolean>;
6152
6182
  }, z.core.$loose>>;
6153
6183
  assignee: z.ZodOptional<z.ZodString>;
6184
+ idempotencyKey: z.ZodOptional<z.ZodString>;
6154
6185
  }, z.core.$loose>;
6155
6186
  };
6156
6187
  readonly response: {
@@ -7993,6 +8024,7 @@ declare const agentApiContract: {
7993
8024
  targetAgent: z.ZodString;
7994
8025
  draftHint: z.ZodOptional<z.ZodString>;
7995
8026
  }, z.core.$strip>], "type">;
8027
+ idempotencyKey: z.ZodOptional<z.ZodString>;
7996
8028
  }, z.core.$loose>;
7997
8029
  };
7998
8030
  readonly response: {
@@ -9908,12 +9940,15 @@ declare const OPERATION_DEFS: readonly [{
9908
9940
  targetAgent: z.ZodString;
9909
9941
  draftHint: z.ZodOptional<z.ZodString>;
9910
9942
  }, z.core.$strip>], "type">;
9943
+ idempotencyKey: z.ZodOptional<z.ZodString>;
9911
9944
  }, z.core.$loose>;
9912
9945
  readonly fieldDescriptions: {
9913
9946
  readonly target: "Conversation to post the card in: `#channel`, `dm:@peer`, or a thread.";
9914
9947
  readonly action: "The operation the card proposes: `type` picks it, and the other fields apply per type as described.";
9948
+ readonly 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.";
9915
9949
  };
9916
9950
  readonly routes: readonly ["actionPrepare"];
9951
+ readonly idempotencyArg: "idempotencyKey";
9917
9952
  readonly consumes: {
9918
9953
  model: never[];
9919
9954
  code: never[];
@@ -10009,8 +10044,10 @@ declare const OPERATION_DEFS: readonly [{
10009
10044
  createsResource: z.ZodOptional<z.ZodBoolean>;
10010
10045
  }, z.core.$strip>>;
10011
10046
  assignee: z.ZodOptional<z.ZodString>;
10047
+ idempotencyKey: z.ZodOptional<z.ZodString>;
10012
10048
  }, z.core.$strip>;
10013
10049
  readonly routes: readonly ["taskCreate"];
10050
+ readonly idempotencyArg: "idempotencyKey";
10014
10051
  readonly consumes: {
10015
10052
  model: never[];
10016
10053
  code: never[];
package/operations.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "raft-sdk-operations.v1",
3
- "version": "90a77ae0a9067bc0",
3
+ "version": "c2d4e5cdeb300d10",
4
4
  "operations": [
5
5
  {
6
6
  "name": "identity.whoami",
@@ -961,6 +961,10 @@
961
961
  "type"
962
962
  ],
963
963
  "description": "The operation the card proposes: `type` picks it, and the other fields apply per type as described."
964
+ },
965
+ "idempotencyKey": {
966
+ "type": "string",
967
+ "description": "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."
964
968
  }
965
969
  },
966
970
  "required": [
@@ -970,7 +974,8 @@
970
974
  },
971
975
  "sideEffect": "write",
972
976
  "idempotency": {
973
- "kind": "none"
977
+ "kind": "key",
978
+ "arg": "idempotencyKey"
974
979
  },
975
980
  "capability": [
976
981
  "tasks"
@@ -1218,6 +1223,10 @@
1218
1223
  "assignee": {
1219
1224
  "type": "string",
1220
1225
  "description": "`@handle`: yourself to start in_progress, or (owner/admin) someone else to reserve a todo."
1226
+ },
1227
+ "idempotencyKey": {
1228
+ "type": "string",
1229
+ "description": "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."
1221
1230
  }
1222
1231
  },
1223
1232
  "required": [
@@ -1227,7 +1236,8 @@
1227
1236
  },
1228
1237
  "sideEffect": "write",
1229
1238
  "idempotency": {
1230
- "kind": "none"
1239
+ "kind": "key",
1240
+ "arg": "idempotencyKey"
1231
1241
  },
1232
1242
  "capability": [
1233
1243
  "tasks"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botiverse/raft-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "FSL-1.1-ALv2",
5
5
  "description": "Typed Raft Agent API client for external agents and bots.",
6
6
  "type": "module",