@copilotkit/channels-core 0.9.0 → 0.9.1

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/dist/thread.js CHANGED
@@ -2,7 +2,17 @@ import { runAgentLoop } from "./run-loop.js";
2
2
  import { errorClass, normalizePlatform } from "./telemetry/sanitize-error.js";
3
3
  import { toAgentToolDescriptors } from "./tools.js";
4
4
  import { validateSchema } from "./standard-schema.js";
5
+ import { channelActorIdentity } from "./identity.js";
5
6
  import { hasMemoryAccess, resolveMemoryGrant } from "./memory.js";
7
+ /**
8
+ * Default retention for a captured interrupt value (7 days) — deliberately the
9
+ * same default the action store uses, so the retained value and the button that
10
+ * consumes it expire together. If a channel configures a LONGER
11
+ * `actionRetentionMs`, the value can lapse before the button does; a resume then
12
+ * simply omits `command.interruptEvent`, which is exactly the pre-existing
13
+ * behaviour rather than a new failure.
14
+ */
15
+ const DEFAULT_INTERRUPT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
6
16
  /** A Channel run requested Memory without an attached Intelligence backend. */
7
17
  export class ChannelMemoryUnavailableError extends Error {
8
18
  code = "channel_memory_unavailable";
@@ -254,6 +264,33 @@ export class Thread {
254
264
  state() {
255
265
  return this.trackOperation(() => this.store.kv.get(`threadstate:${this.deps.conversationKey}`));
256
266
  }
267
+ interruptEventKey() {
268
+ return `interruptevent:${this.deps.conversationKey}`;
269
+ }
270
+ /**
271
+ * Retain the raw value of a captured interrupt so a later {@link resume} can
272
+ * echo it back to the agent as `command.interruptEvent`.
273
+ *
274
+ * PERSISTED, not in-memory, because the resume arrives in a DIFFERENT run —
275
+ * possibly in a different process after a restart, which is the whole reason
276
+ * this HITL path exists. Stored per conversation, matching the fact that a
277
+ * renderer tracks a single pending interrupt: a second interrupt in the same
278
+ * conversation legitimately supersedes the first.
279
+ *
280
+ * Best-effort: an agent that needs no correlation data (LangGraph) resumes
281
+ * fine without this, so a store failure must not take the interrupt down.
282
+ */
283
+ async rememberInterruptEvent(value) {
284
+ if (value === undefined)
285
+ return;
286
+ try {
287
+ await this.store.kv.set(this.interruptEventKey(), value, this.deps.interruptRetentionMs ?? DEFAULT_INTERRUPT_RETENTION_MS);
288
+ }
289
+ catch (err) {
290
+ console.warn("[channel] could not retain the interrupt value; a resume will omit " +
291
+ "command.interruptEvent (fine for LangGraph, breaks Mastra):", err);
292
+ }
293
+ }
257
294
  /** Read the conversation's messages (returns `[]` when the adapter can't read history). */
258
295
  getMessages() {
259
296
  return this.trackOperation(async () => (await this.deps.adapter.getMessages?.(this.deps.replyTarget)) ?? []);
@@ -348,8 +385,28 @@ export class Thread {
348
385
  initiator: claimed.initiator,
349
386
  };
350
387
  this.activeContinuation = continuation;
388
+ // Echo the originating interrupt back to the agent. Some AG-UI bridges
389
+ // (e.g. @ag-ui/mastra) key their resume off it — it carries the correlation
390
+ // ids identifying WHICH suspended call to continue — and silently ignore a
391
+ // resume without it. LangGraph needs no correlation data and ignores the
392
+ // extra field, so this is additive for existing agents.
393
+ //
394
+ // `consume` is atomic take-and-delete, matching the one-use continuation
395
+ // claimed just above: a replayed click must not resurrect a spent resume.
396
+ // Omitted entirely when absent, so the wire shape is byte-identical to
397
+ // before for any flow that never captured an interrupt value.
398
+ let interruptEvent;
399
+ try {
400
+ interruptEvent = await this.store.kv.consume(this.interruptEventKey());
401
+ }
402
+ catch (err) {
403
+ console.warn("[channel] could not read the retained interrupt value; resuming " +
404
+ "without command.interruptEvent:", err);
405
+ }
351
406
  try {
352
- return await this.run({ resume: value }, { memory });
407
+ return await this.run(interruptEvent === undefined
408
+ ? { resume: value }
409
+ : { resume: value, interruptEvent }, { memory });
353
410
  }
354
411
  finally {
355
412
  if (this.activeContinuation === continuation) {
@@ -480,6 +537,7 @@ export class Thread {
480
537
  // reported as agent_run_failed (with the right stage) instead of being
481
538
  // hidden behind an already-sent success event.
482
539
  let stage = "agent";
540
+ const identity = channelActorIdentity(this.deps.actor, this.platform);
483
541
  try {
484
542
  const loopArgs = {
485
543
  agent: session.agent,
@@ -487,6 +545,9 @@ export class Thread {
487
545
  tools,
488
546
  toolDescriptors,
489
547
  context,
548
+ // The platform's word for who spoke, taken from this Thread's ingress
549
+ // rather than from the run's caller. See `channelActorIdentity`.
550
+ ...(identity ? { identity } : {}),
490
551
  makeToolCtx: () => ({
491
552
  thread: this,
492
553
  message: this.deps.message,
@@ -496,6 +557,10 @@ export class Thread {
496
557
  platform: this.platform,
497
558
  }),
498
559
  handleInterrupt: async (interrupt) => {
560
+ // Retain BEFORE dispatching: the handler posts the picker, and the
561
+ // click that resumes it can arrive at any later moment, so the value
562
+ // has to already be durable by the time the button exists.
563
+ await this.rememberInterruptEvent(interrupt.value);
499
564
  const h = this.deps.interruptHandlers.get(interrupt.eventName);
500
565
  if (h)
501
566
  await h({
@@ -515,6 +580,7 @@ export class Thread {
515
580
  tools: toolDescriptors,
516
581
  context,
517
582
  isResume: initialResume !== undefined,
583
+ user: this.deps.user,
518
584
  memory: extra?.memory,
519
585
  execute: (subscriber, canonicalRun) => runAgentLoop({
520
586
  ...loopArgs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@copilotkit/channels-core",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Platform-agnostic JSX channel engine for CopilotKit (createChannel, Thread, PlatformAdapter, ActionStore).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,12 +48,12 @@
48
48
  "dist"
49
49
  ],
50
50
  "dependencies": {
51
- "@ag-ui/client": "0.0.57",
52
- "@ag-ui/core": "0.0.57",
51
+ "@ag-ui/client": "0.0.59",
52
+ "@ag-ui/core": "0.0.59",
53
53
  "zod-to-json-schema": "^3.24.1",
54
- "@copilotkit/channels-ui": "~0.9.0",
55
- "@copilotkit/shared": "^1.68.0",
56
- "@copilotkit/core": "^1.68.0"
54
+ "@copilotkit/channels-ui": "~0.9.1",
55
+ "@copilotkit/core": "^1.70.0",
56
+ "@copilotkit/shared": "^1.70.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^22.10.0",