@falkenslab/agent-kit 0.33.0 → 0.34.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.
@@ -89,6 +89,8 @@ export interface TranscriptCall {
89
89
  isError: boolean;
90
90
  text: string;
91
91
  } | null;
92
+ /** An MCP result's `_meta`, untouched: data for this view, which the model never sees (absent when the tool returned none). */
93
+ meta?: Record<string, unknown>;
92
94
  /** What a subagent it started did, by label. */
93
95
  children: string[];
94
96
  }
@@ -251,6 +253,14 @@ export declare function createChatController(options: Options | SessionOpener, s
251
253
  * goes to the agent as it is, never read as one of the chat's commands (`/extensions`, `/resume`).
252
254
  */
253
255
  turn(raw: string): Promise<void>;
256
+ /**
257
+ * A message for the agent while a turn runs (#92), without stopping it: it joins the turn at
258
+ * its next tool call, or is answered right after when no call is left, and it stays in the
259
+ * transcript where it was sent. The turn `send()` is waiting for lasts until it's answered.
260
+ * False, and nothing sent, when no turn is running (use `send()`) and for a slash command or
261
+ * an exit line, which wait for the turn to end.
262
+ */
263
+ steer(raw: string): Promise<boolean>;
254
264
  /** Stops the turn running, if any. */
255
265
  interrupt(): void;
256
266
  /** Switches to a mode the session allows; says so when it can't. */
@@ -4,7 +4,7 @@ import { getInteractionPort, setInteractionPort } from "../core/interaction.js";
4
4
  import { getLanguage, switchLanguage, t } from "../core/messages/index.js";
5
5
  import { LANGUAGE_NAMES } from "../core/language.js";
6
6
  import { runQuery } from "../core/runner.js";
7
- import { createRunFolder, listRuns, readConversation, readRunSession } from "../core/runs.js";
7
+ import { createRunFolder, listRuns, readConversationSteps, readRunSession } from "../core/runs.js";
8
8
  import { createInputQueue, togglePlanMode } from "../core/session.js";
9
9
  import { hasOpenTodos, parseTodos, TODO_TOOL } from "../core/todos.js";
10
10
  import { withToolLabels, withToolPhrases, withToolResults } from "../core/toolLabels.js";
@@ -96,15 +96,18 @@ export async function createChatController(options, settings = {}) {
96
96
  addEntry({ id: nextId++, kind: "notice", tone, text });
97
97
  emit({ type: "notice", tone, text });
98
98
  }
99
+ // A reply of its own coming: the answer to a message sent into the turn, after a `turn-end`.
100
+ let newReply = false;
99
101
  /** What the agent does, into the transcript. */
100
102
  function record(event) {
101
103
  const last = state.transcript.at(-1);
102
104
  switch (event.type) {
103
105
  case "text":
104
- if (last?.kind === "agent")
106
+ if (last?.kind === "agent" && !newReply)
105
107
  update({ transcript: [...state.transcript.slice(0, -1), { ...last, text: last.text + event.text }] });
106
108
  else
107
109
  addEntry({ id: nextId++, kind: "agent", text: event.text });
110
+ newReply = false;
108
111
  return;
109
112
  case "action": {
110
113
  const todos = event.toolName === TODO_TOOL ? parseTodos(event.input) : null;
@@ -129,7 +132,7 @@ export async function createChatController(options, settings = {}) {
129
132
  ? entry
130
133
  : {
131
134
  ...entry,
132
- calls: entry.calls.map((call) => call.id !== id ? call : event.type === "tool-result" ? { ...call, result: { isError: event.isError, text: event.text } } : { ...call, children: [...call.children, label] }),
135
+ calls: entry.calls.map((call) => call.id !== id ? call : event.type === "tool-result" ? { ...call, result: { isError: event.isError, text: event.text }, ...(event.meta ? { meta: event.meta } : {}) } : { ...call, children: [...call.children, label] }),
133
136
  });
134
137
  update({ transcript, ...(label ? { subagentActivity: label } : {}) });
135
138
  return;
@@ -160,9 +163,17 @@ export async function createChatController(options, settings = {}) {
160
163
  let readError = null;
161
164
  let unsubscribeMode;
162
165
  let interrupted = false;
166
+ // A message sent into this turn (`steer()`): its `turn-end` may not be the last, so the turn
167
+ // ends at `session-idle`, once a `turn-end` has come after the latest message sent. Before
168
+ // that, an idle is an earlier one, still on its way.
169
+ let steered = false;
170
+ let awaitingIdle = false;
163
171
  function finishTurn() {
164
172
  const done = turnDone;
165
173
  turnDone = null;
174
+ newReply = false;
175
+ steered = false;
176
+ awaitingIdle = false;
166
177
  done?.();
167
178
  }
168
179
  /** The commands the session has, and the chat's own, into the state: when it opens, and when an extension is switched in it. */
@@ -214,12 +225,25 @@ export async function createChatController(options, settings = {}) {
214
225
  const { value, done } = await events.next();
215
226
  if (own !== generation || done)
216
227
  break;
228
+ if (value.type === "session-idle") {
229
+ if (awaitingIdle)
230
+ finishTurn();
231
+ continue;
232
+ }
217
233
  if (value.type !== "prompt-suggestion")
218
234
  logRenderer.render(value);
219
235
  record(value);
220
236
  emit({ type: "agent", event: value });
221
- if (value.type === "turn-end")
222
- finishTurn();
237
+ if (value.type === "turn-end") {
238
+ if (!steered)
239
+ finishTurn();
240
+ else {
241
+ awaitingIdle = true;
242
+ newReply = true;
243
+ // What's left to answer can be interrupted too.
244
+ interrupted = false;
245
+ }
246
+ }
223
247
  }
224
248
  }
225
249
  catch (error) {
@@ -238,6 +262,12 @@ export async function createChatController(options, settings = {}) {
238
262
  // Extensions turned off or on in the running session, told to the model with the next message
239
263
  // too: the prompt's Extensions section still says what the session opened with.
240
264
  const extensionNotices = [];
265
+ /** A line with what the model hasn't been told yet before it: each note goes once. */
266
+ function withNotices(line) {
267
+ const notices = [languageNotice, ...extensionNotices.splice(0)].filter(Boolean);
268
+ languageNotice = null;
269
+ return notices.length ? `${notices.join("\n")}\n\n${line}` : line;
270
+ }
241
271
  async function runTurn(line) {
242
272
  interrupted = false;
243
273
  const from = state.transcript.length;
@@ -246,9 +276,7 @@ export async function createChatController(options, settings = {}) {
246
276
  update({ busy: true, turnStartedAt: startedAt, activity: null, subagentActivity: null, suggestion: null });
247
277
  emit({ type: "turn-start" });
248
278
  const finished = new Promise((resolve) => (turnDone = resolve));
249
- const notices = [languageNotice, ...extensionNotices.splice(0)].filter(Boolean);
250
- queue.push(notices.length ? `${notices.join("\n")}\n\n${line}` : line);
251
- languageNotice = null;
279
+ queue.push(withNotices(line));
252
280
  await finished;
253
281
  logRenderer.endLine();
254
282
  latestReply = state.transcript.slice(from).flatMap((entry) => (entry.kind === "agent" ? [entry.text] : [])).join("").trim();
@@ -264,10 +292,26 @@ export async function createChatController(options, settings = {}) {
264
292
  async function showConversation(folder, fromResume) {
265
293
  if (!folder.sessionId)
266
294
  return;
267
- const messages = await readConversation(folder.dir);
295
+ const steps = await readConversationSteps(folder.dir);
296
+ const messages = steps.filter((step) => step.role !== "tool");
268
297
  const [summary] = (await listRuns(path.dirname(folder.dir))).filter((other) => path.resolve(other.dir) === path.resolve(folder.dir));
269
298
  const updatedAt = summary?.updatedAt ?? new Date();
270
- const replayed = messages.map((message) => ({ id: nextId++, kind: message.role === "user" ? "user" : "agent", text: message.text }));
299
+ const replayed = [];
300
+ for (const step of steps) {
301
+ if (step.role !== "tool") {
302
+ replayed.push({ id: nextId++, kind: step.role === "user" ? "user" : "agent", text: step.text });
303
+ continue;
304
+ }
305
+ if (step.toolName === TODO_TOOL)
306
+ continue;
307
+ // A call the session ended in the middle of has no result; `null` would read as still running.
308
+ const call = { id: step.id, toolName: step.toolName, label: formatAction(step.toolName, step.input), result: step.result ?? { isError: false, text: "" }, children: [], ...(step.meta ? { meta: step.meta } : {}) };
309
+ const last = replayed.at(-1);
310
+ if (last?.kind === "tools")
311
+ last.calls.push(call);
312
+ else
313
+ replayed.push({ id: nextId++, kind: "tools", calls: [call] });
314
+ }
271
315
  update({ transcript: fromResume ? replayed : [...state.transcript, ...replayed], turns: (fromResume ? 0 : state.turns) + messages.filter((message) => message.role === "user").length });
272
316
  emit({ type: "conversation", messages, updatedAt, fromResume });
273
317
  }
@@ -364,6 +408,20 @@ export async function createChatController(options, settings = {}) {
364
408
  }
365
409
  await controller.trustWorkspace();
366
410
  }
411
+ /** A line of the person's, in the session log, the transcript and to the views. */
412
+ function showLine(line) {
413
+ log(`${promptLabel}${line}\n`);
414
+ addEntry({ id: nextId++, kind: "user", text: line });
415
+ emit({ type: "user", text: line });
416
+ }
417
+ /** A line of the person's, kept for ↑/↓. */
418
+ async function remember(line) {
419
+ if (!settings.historyPath)
420
+ return;
421
+ historyEntries = capHistory([...historyEntries, { text: line, timestamp: new Date().toISOString() }], historyLimit);
422
+ update({ history: historyEntries.map((entry) => entry.text) });
423
+ await saveHistory(settings.historyPath, historyEntries);
424
+ }
367
425
  // A choice in place of the prompt, when the view doesn't ask it itself.
368
426
  let choiceResolve = null;
369
427
  function pick(title, options) {
@@ -455,14 +513,8 @@ export async function createChatController(options, settings = {}) {
455
513
  const line = raw.trim();
456
514
  if (!line)
457
515
  return;
458
- log(`${promptLabel}${line}\n`);
459
- addEntry({ id: nextId++, kind: "user", text: line });
460
- emit({ type: "user", text: line });
461
- if (settings.historyPath) {
462
- historyEntries = capHistory([...historyEntries, { text: line, timestamp: new Date().toISOString() }], historyLimit);
463
- update({ history: historyEntries.map((entry) => entry.text) });
464
- await saveHistory(settings.historyPath, historyEntries);
465
- }
516
+ showLine(line);
517
+ await remember(line);
466
518
  const lower = line.toLowerCase();
467
519
  if (exitCommands.has(lower))
468
520
  return "exit";
@@ -501,11 +553,28 @@ export async function createChatController(options, settings = {}) {
501
553
  const line = raw.trim();
502
554
  if (!line)
503
555
  return;
504
- log(`${promptLabel}${line}\n`);
505
- addEntry({ id: nextId++, kind: "user", text: line });
506
- emit({ type: "user", text: line });
556
+ showLine(line);
507
557
  await runTurn(line);
508
558
  },
559
+ /**
560
+ * A message for the agent while a turn runs (#92), without stopping it: it joins the turn at
561
+ * its next tool call, or is answered right after when no call is left, and it stays in the
562
+ * transcript where it was sent. The turn `send()` is waiting for lasts until it's answered.
563
+ * False, and nothing sent, when no turn is running (use `send()`) and for a slash command or
564
+ * an exit line, which wait for the turn to end.
565
+ */
566
+ async steer(raw) {
567
+ const line = raw.trim();
568
+ if (!line || !turnDone || exitCommands.has(line.toLowerCase()) || slashCommandToken(line))
569
+ return false;
570
+ showLine(line);
571
+ queue.push(withNotices(line), { priority: "next" });
572
+ steered = true;
573
+ // Any idle on its way is from before this message.
574
+ awaitingIdle = false;
575
+ await remember(line);
576
+ return true;
577
+ },
509
578
  /** Stops the turn running, if any. */
510
579
  interrupt() {
511
580
  if (!state.busy || interrupted)
@@ -43,13 +43,16 @@ export type AgentEvent =
43
43
  }
44
44
  /** What one of the main agent's tool calls returned, paired with its `action` by
45
45
  * `toolUseId`, as plain text (images and other non-text parts left out). Subagents'
46
- * results stay out, as their actions do. A console log ignores it. */
46
+ * results stay out, as their actions do. A console log ignores it. `meta` is an MCP
47
+ * result's `_meta`, untouched: data for the client, which the model never sees and the kit
48
+ * doesn't interpret (absent when the tool returned none). */
47
49
  | {
48
50
  type: "tool-result";
49
51
  toolUseId: string;
50
52
  toolName: string;
51
53
  isError: boolean;
52
54
  text: string;
55
+ meta?: Record<string, unknown>;
53
56
  }
54
57
  /** One or more MCP servers failed to connect at session startup. */
55
58
  | {
@@ -88,6 +91,12 @@ export type AgentEvent =
88
91
  | {
89
92
  type: "prompt-suggestion";
90
93
  suggestion: string;
94
+ }
95
+ /** Nothing is left to answer. It arrives once, after the last `turn-end`: a message sent
96
+ * into a running turn with no tool call left gets a `turn-end` of its own, so a `turn-end`
97
+ * doesn't say the work is over, and this does (confirmed empirically, SDK 0.3.300). */
98
+ | {
99
+ type: "session-idle";
91
100
  };
92
101
  /**
93
102
  * Running totals for the whole `query()` session so far, not for one turn: the SDK's own
@@ -114,6 +123,12 @@ export interface SessionUsage {
114
123
  export declare function visibleErrors(errors: readonly string[]): string[];
115
124
  /** A tool result's content as plain text: its text parts joined, anything else skipped. */
116
125
  export declare function toolResultText(content: unknown): string;
126
+ /**
127
+ * An MCP result's `_meta`, from what holds it: a stream message's `tool_use_result` or a stored
128
+ * entry's `mcpMeta` (confirmed empirically, SDK 0.3.300: neither `tool_result` nor a
129
+ * `PostToolUse` hook carries it).
130
+ */
131
+ export declare function toolResultMeta(holder: unknown): Record<string, unknown> | undefined;
117
132
  /** How full the session's context window is (see `AgentRun.contextUsage()`). */
118
133
  export interface ContextUsage {
119
134
  /** 0-100. */
@@ -19,6 +19,17 @@ export function toolResultText(content) {
19
19
  .map((part) => part.text)
20
20
  .join("\n");
21
21
  }
22
+ /**
23
+ * An MCP result's `_meta`, from what holds it: a stream message's `tool_use_result` or a stored
24
+ * entry's `mcpMeta` (confirmed empirically, SDK 0.3.300: neither `tool_result` nor a
25
+ * `PostToolUse` hook carries it).
26
+ */
27
+ export function toolResultMeta(holder) {
28
+ if (typeof holder !== "object" || holder === null)
29
+ return undefined;
30
+ const meta = holder._meta;
31
+ return typeof meta === "object" && meta !== null && !Array.isArray(meta) ? meta : undefined;
32
+ }
22
33
  /**
23
34
  * The SDK's sign-in methods for a remote MCP server behind OAuth, when its `Query` has them: they
24
35
  * aren't in its typings (confirmed empirically, SDK 0.3.300; ADR-028), so each is checked first.
@@ -66,7 +77,9 @@ async function restoreSavedSwitches(result, options) {
66
77
  * `close`, `supportedCommands`) is passed through unchanged.
67
78
  */
68
79
  export function runQuery(prompt, options) {
69
- const result = query({ prompt, options });
80
+ // The session's state, for `session-idle`: the CLI only reports it when asked (confirmed
81
+ // empirically, SDK 0.3.300). `env` replaces the subprocess environment, so the rest goes on.
82
+ const result = query({ prompt, options: { ...options, env: { ...(options.env ?? process.env), CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1" } } });
70
83
  // What the session is, for an extension that asks (sessionFacts.ts): the controls now, the
71
84
  // tools and skills when the session starts.
72
85
  const view = sessionViewOf(options);
@@ -95,6 +108,11 @@ export function runQuery(prompt, options) {
95
108
  yield { type: "info", text: message.content, level: message.level };
96
109
  continue;
97
110
  }
111
+ if (message.type === "system" && message.subtype === "session_state_changed") {
112
+ if (message.state === "idle")
113
+ yield { type: "session-idle" };
114
+ continue;
115
+ }
98
116
  if (message.type === "system" && message.subtype === "local_command_output") {
99
117
  yield { type: "info", text: message.content, level: "local-command" };
100
118
  continue;
@@ -124,13 +142,15 @@ export function runQuery(prompt, options) {
124
142
  continue;
125
143
  }
126
144
  if (message.type === "user" && message.parent_tool_use_id === null && Array.isArray(message.message.content)) {
145
+ // One result per message in practice; were there several, `tool_use_result` couldn't be told apart.
146
+ const meta = message.message.content.filter((block) => block.type === "tool_result").length === 1 ? toolResultMeta(message.tool_use_result) : undefined;
127
147
  for (const block of message.message.content) {
128
148
  if (block.type !== "tool_result")
129
149
  continue;
130
150
  const toolName = toolNames.get(block.tool_use_id);
131
151
  if (!toolName)
132
152
  continue;
133
- yield { type: "tool-result", toolUseId: block.tool_use_id, toolName, isError: block.is_error === true, text: toolResultText(block.content) };
153
+ yield { type: "tool-result", toolUseId: block.tool_use_id, toolName, isError: block.is_error === true, text: toolResultText(block.content), ...(meta ? { meta } : {}) };
134
154
  }
135
155
  }
136
156
  if (message.type === "prompt_suggestion") {
@@ -23,6 +23,20 @@ export interface ConversationMessage {
23
23
  role: "user" | "assistant";
24
24
  text: string;
25
25
  }
26
+ /** One tool call of the main agent's in a kept conversation, with what came back. */
27
+ export interface ConversationToolCall {
28
+ role: "tool";
29
+ id: string;
30
+ toolName: string;
31
+ input: unknown;
32
+ /** Null when the session ended before the result. */
33
+ result: {
34
+ isError: boolean;
35
+ text: string;
36
+ } | null;
37
+ /** An MCP result's `_meta`, as the live `tool-result` event has it. */
38
+ meta?: Record<string, unknown>;
39
+ }
26
40
  /** A folder name for a new run, e.g. "2026-09-09T16-50-12-345Z" (sorts by date). */
27
41
  export declare function runFolderName(date?: Date): string;
28
42
  /** Creates a new, empty run folder under `runsDir`. */
@@ -47,6 +61,8 @@ export declare function createRunStore(dir: string): SessionStore;
47
61
  export declare function entryText(entry: SessionStoreEntry): ConversationMessage | null;
48
62
  /** The human's messages and the agent's replies in a run's conversation, in order. */
49
63
  export declare function readConversation(dir: string): Promise<ConversationMessage[]>;
64
+ /** A run's conversation with the main agent's tool calls in their place: what a view shows of a resumed one. */
65
+ export declare function readConversationSteps(dir: string): Promise<(ConversationMessage | ConversationToolCall)[]>;
50
66
  /** The runs under `runsDir` that kept a conversation, newest first; runs without `session.json` are left out. */
51
67
  export declare function listRuns(runsDir: string): Promise<RunSummary[]>;
52
68
  /** `--continue` among the process's arguments: start by resuming the latest run. */
package/dist/core/runs.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { toolResultMeta, toolResultText } from "./runner.js";
3
4
  const CONVERSATION = "conversation.jsonl";
4
5
  const SUBAGENTS = "subagents";
5
6
  const SESSION = "session.json";
@@ -133,6 +134,13 @@ export function createRunStore(dir) {
133
134
  }
134
135
  /** A transcript entry's text when it's something the human wrote or the agent replied, or null. */
135
136
  export function entryText(entry) {
137
+ // A message sent into a running turn is kept as a queued command, not as a user entry
138
+ // (confirmed empirically, SDK 0.3.300).
139
+ const queued = entry.type === "attachment" ? entry.attachment : undefined;
140
+ if (queued?.type === "queued_command" && queued.commandMode === "prompt" && typeof queued.prompt === "string") {
141
+ const text = humanText(queued.prompt);
142
+ return text ? { role: "user", text } : null;
143
+ }
136
144
  if (entry.type !== "user" && entry.type !== "assistant")
137
145
  return null;
138
146
  if (entry.isMeta || entry.isSidechain)
@@ -151,18 +159,52 @@ export function entryText(entry) {
151
159
  .map((part) => part.text ?? "")
152
160
  .join("");
153
161
  }
162
+ const shown = humanText(text);
163
+ return shown ? { role: entry.type, text: shown } : null;
164
+ }
165
+ /** A message's text as a person reads it, or null when nothing of it is theirs. */
166
+ function humanText(raw) {
154
167
  // The kit's own notes to the model (a plan-mode switch) go before the human's words.
155
- text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "");
168
+ const text = raw.replace(/<system-reminder>[\s\S]*?<\/system-reminder>\s*/g, "");
156
169
  // Slash commands and the CLI's own notes arrive as tagged text, not as the human's words.
157
- if (!text.trim() || /^\s*<[a-z-]+>/.test(text))
158
- return null;
159
- return { role: entry.type, text };
170
+ return !text.trim() || /^\s*<[a-z-]+>/.test(text) ? null : text;
160
171
  }
161
172
  /** The human's messages and the agent's replies in a run's conversation, in order. */
162
173
  export async function readConversation(dir) {
163
174
  const entries = (await readEntries(path.join(dir, CONVERSATION))) ?? [];
164
175
  return entries.map(entryText).filter((message) => message !== null);
165
176
  }
177
+ /** A run's conversation with the main agent's tool calls in their place: what a view shows of a resumed one. */
178
+ export async function readConversationSteps(dir) {
179
+ const entries = (await readEntries(path.join(dir, CONVERSATION))) ?? [];
180
+ const steps = [];
181
+ const calls = new Map();
182
+ for (const entry of entries) {
183
+ const message = entryText(entry);
184
+ if (message)
185
+ steps.push(message);
186
+ const content = entry.message?.content;
187
+ if (entry.isSidechain || !Array.isArray(content))
188
+ continue;
189
+ for (const part of content) {
190
+ if (entry.type === "assistant" && part.type === "tool_use" && part.id && part.name && !calls.has(part.id)) {
191
+ const call = { role: "tool", id: part.id, toolName: part.name, input: part.input, result: null };
192
+ calls.set(part.id, call);
193
+ steps.push(call);
194
+ }
195
+ else if (entry.type === "user" && part.type === "tool_result" && part.tool_use_id) {
196
+ const call = calls.get(part.tool_use_id);
197
+ if (!call)
198
+ continue;
199
+ call.result = { isError: part.is_error === true, text: toolResultText(part.content) };
200
+ const meta = toolResultMeta(entry.mcpMeta);
201
+ if (meta)
202
+ call.meta = meta;
203
+ }
204
+ }
205
+ }
206
+ return steps;
207
+ }
166
208
  /** The runs under `runsDir` that kept a conversation, newest first; runs without `session.json` are left out. */
167
209
  export async function listRuns(runsDir) {
168
210
  let names;
@@ -118,11 +118,17 @@ export declare function pluginSkills(roots: readonly string[]): Promise<string[]
118
118
  *
119
119
  * With `modeControl`, each message goes after the note on a plan-mode switch the model
120
120
  * hasn't been told about yet (`ModeControl.takeNotice()`).
121
+ *
122
+ * `push(text, { priority: "next" })` is for a turn already running: the message joins it at its
123
+ * next tool boundary without stopping it, or gets a turn of its own when no tool call is left
124
+ * (confirmed empirically, SDK 0.3.300; `"now"` would cut the running tool).
121
125
  */
122
126
  export declare function createInputQueue(options?: {
123
127
  modeControl?: ModeControl;
124
128
  }): {
125
- push: (text: string) => void;
129
+ push: (text: string, delivery?: {
130
+ priority?: "next";
131
+ }) => void;
126
132
  end: () => void;
127
133
  iterable: AsyncIterable<SDKUserMessage>;
128
134
  };
@@ -609,6 +609,10 @@ async function projectSkills(projectDir) {
609
609
  *
610
610
  * With `modeControl`, each message goes after the note on a plan-mode switch the model
611
611
  * hasn't been told about yet (`ModeControl.takeNotice()`).
612
+ *
613
+ * `push(text, { priority: "next" })` is for a turn already running: the message joins it at its
614
+ * next tool boundary without stopping it, or gets a turn of its own when no tool call is left
615
+ * (confirmed empirically, SDK 0.3.300; `"now"` would cut the running tool).
612
616
  */
613
617
  export function createInputQueue(options = {}) {
614
618
  const pending = [];
@@ -616,8 +620,8 @@ export function createInputQueue(options = {}) {
616
620
  let ended = false;
617
621
  async function* generator() {
618
622
  while (!ended) {
619
- const text = pending.shift();
620
- if (text === undefined) {
623
+ const next = pending.shift();
624
+ if (next === undefined) {
621
625
  await new Promise((resolve) => {
622
626
  wake = resolve;
623
627
  });
@@ -625,15 +629,16 @@ export function createInputQueue(options = {}) {
625
629
  }
626
630
  yield {
627
631
  type: "user",
628
- message: { role: "user", content: text },
632
+ message: { role: "user", content: next.text },
629
633
  parent_tool_use_id: null,
634
+ ...(next.priority ? { priority: next.priority } : {}),
630
635
  };
631
636
  }
632
637
  }
633
638
  return {
634
- push(text) {
639
+ push(text, delivery = {}) {
635
640
  const notice = options.modeControl?.takeNotice?.();
636
- pending.push(notice ? `${notice}\n\n${text}` : text);
641
+ pending.push({ text: notice ? `${notice}\n\n${text}` : text, ...delivery });
637
642
  wake?.();
638
643
  wake = null;
639
644
  },
@@ -59,6 +59,7 @@ export function createConsoleRenderer(options = {}) {
59
59
  case "subagent-action":
60
60
  case "tool-result":
61
61
  case "prompt-suggestion":
62
+ case "session-idle":
62
63
  return;
63
64
  case "mcp-error":
64
65
  writeLine(ui.warn(t().mcpFailed(event.failedServers.join(", "))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "description": "Library for building an agent on top of the Claude Agent SDK",
5
5
  "author": "Francisco Vargas Ruiz",
6
6
  "license": "MIT",