@falkenslab/agent-kit 0.32.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.
- package/README.md +1 -1
- package/dist/chat/chatController.d.ts +15 -0
- package/dist/chat/chatController.js +100 -18
- package/dist/core/mcpDepth.d.ts +9 -0
- package/dist/core/mcpDepth.js +12 -0
- package/dist/core/runner.d.ts +16 -1
- package/dist/core/runner.js +22 -2
- package/dist/core/runs.d.ts +16 -0
- package/dist/core/runs.js +46 -4
- package/dist/core/session.d.ts +7 -1
- package/dist/core/session.js +10 -5
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/serveMcp.d.ts +28 -0
- package/dist/mcp/serveMcp.js +177 -0
- package/dist/tui/consoleRenderer.js +1 -0
- package/package.json +13 -1
package/README.md
CHANGED
|
@@ -120,7 +120,7 @@ See [everything he can do](https://falkenslab.github.io/agent-kit/docs/examples/
|
|
|
120
120
|
| 🧩 Extensions | Claude Code plugins with MCP servers (local, npm, remote with OAuth), skills, commands and subagents; scopes, a lock, workspace trust, options, marketplaces and a catalog |
|
|
121
121
|
| 🛡️ Security | A file scope gate, denied paths, subagent gates, a remote-servers gate, isolation from the runner's own Claude Code, redacted transcripts |
|
|
122
122
|
| 🤖 Subagents | Declared types only, in the foreground, without Bash for the main agent |
|
|
123
|
-
| 🖥️ Interfaces | A full-screen Ink chat, a plain readline chat, a progress view, a wizard, themes; a chat controller for desktop apps and web pages |
|
|
123
|
+
| 🖥️ Interfaces | A full-screen Ink chat, a plain readline chat, a progress view, a wizard, themes; a chat controller for desktop apps and web pages; an MCP server, so other agents can ask it things |
|
|
124
124
|
| 🌐 Languages | English, Spanish, French and German, for the interface, the replies and the extensions' servers |
|
|
125
125
|
|
|
126
126
|
Every export, with what it takes and what it gives, is in the [API reference](https://falkenslab.github.io/agent-kit/docs/api).
|
|
@@ -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
|
}
|
|
@@ -246,6 +248,19 @@ export declare function createChatController(options: Options | SessionOpener, s
|
|
|
246
248
|
* resolved when it ends. An unknown slash command is caught before it reaches the model.
|
|
247
249
|
*/
|
|
248
250
|
send(raw: string): Promise<"exit" | void>;
|
|
251
|
+
/**
|
|
252
|
+
* A turn that doesn't come from the person's keyboard (another agent's, over MCP): the text
|
|
253
|
+
* goes to the agent as it is, never read as one of the chat's commands (`/extensions`, `/resume`).
|
|
254
|
+
*/
|
|
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>;
|
|
249
264
|
/** Stops the turn running, if any. */
|
|
250
265
|
interrupt(): void;
|
|
251
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
459
|
-
|
|
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";
|
|
@@ -493,6 +545,36 @@ export async function createChatController(options, settings = {}) {
|
|
|
493
545
|
}
|
|
494
546
|
await runTurn(line);
|
|
495
547
|
},
|
|
548
|
+
/**
|
|
549
|
+
* A turn that doesn't come from the person's keyboard (another agent's, over MCP): the text
|
|
550
|
+
* goes to the agent as it is, never read as one of the chat's commands (`/extensions`, `/resume`).
|
|
551
|
+
*/
|
|
552
|
+
async turn(raw) {
|
|
553
|
+
const line = raw.trim();
|
|
554
|
+
if (!line)
|
|
555
|
+
return;
|
|
556
|
+
showLine(line);
|
|
557
|
+
await runTurn(line);
|
|
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
|
+
},
|
|
496
578
|
/** Stops the turn running, if any. */
|
|
497
579
|
interrupt() {
|
|
498
580
|
if (!state.busy || interrupted)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How deep in a chain of agents served over MCP this process is (ADR-027): an agent that serves
|
|
3
|
+
* itself reads it, and passes it on one higher to the servers it starts, so a loop (A asks B asks
|
|
4
|
+
* A) stops instead of spawning agents forever. An installed extension's server never gets it: its
|
|
5
|
+
* environment is stripped, and no server should depend on the kit's variables.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MCP_DEPTH_ENV = "AGENT_KIT_MCP_DEPTH";
|
|
8
|
+
/** The depth this process runs at: 0 when no agent served over MCP started it. */
|
|
9
|
+
export declare function mcpDepth(): number;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How deep in a chain of agents served over MCP this process is (ADR-027): an agent that serves
|
|
3
|
+
* itself reads it, and passes it on one higher to the servers it starts, so a loop (A asks B asks
|
|
4
|
+
* A) stops instead of spawning agents forever. An installed extension's server never gets it: its
|
|
5
|
+
* environment is stripped, and no server should depend on the kit's variables.
|
|
6
|
+
*/
|
|
7
|
+
export const MCP_DEPTH_ENV = "AGENT_KIT_MCP_DEPTH";
|
|
8
|
+
/** The depth this process runs at: 0 when no agent served over MCP started it. */
|
|
9
|
+
export function mcpDepth() {
|
|
10
|
+
const depth = Number(process.env[MCP_DEPTH_ENV]);
|
|
11
|
+
return Number.isInteger(depth) && depth > 0 ? depth : 0;
|
|
12
|
+
}
|
package/dist/core/runner.d.ts
CHANGED
|
@@ -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. */
|
package/dist/core/runner.js
CHANGED
|
@@ -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
|
-
|
|
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") {
|
package/dist/core/runs.d.ts
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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;
|
package/dist/core/session.d.ts
CHANGED
|
@@ -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
|
|
129
|
+
push: (text: string, delivery?: {
|
|
130
|
+
priority?: "next";
|
|
131
|
+
}) => void;
|
|
126
132
|
end: () => void;
|
|
127
133
|
iterable: AsyncIterable<SDKUserMessage>;
|
|
128
134
|
};
|
package/dist/core/session.js
CHANGED
|
@@ -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
|
|
620
|
-
if (
|
|
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
|
},
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { serveMcp, type ServeMcpSettings } from "./serveMcp.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { serveMcp } from "./serveMcp.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
2
|
+
import type { AgentIdentity } from "../core/agentSpec.js";
|
|
3
|
+
import { type ChatSettings } from "../chat/chatController.js";
|
|
4
|
+
import { type SessionOpener } from "../chat/runs.js";
|
|
5
|
+
/** What `serveMcp()` needs besides how to open a session. */
|
|
6
|
+
export interface ServeMcpSettings {
|
|
7
|
+
/** Where the runs are: a `session_id` names one of them. */
|
|
8
|
+
runsDir: string;
|
|
9
|
+
/** Who the agent is: the server's name and version, and what the `ask` tool says it does. */
|
|
10
|
+
identity: AgentIdentity;
|
|
11
|
+
/** How many agents served over MCP may chain before `ask` refuses (default 3). */
|
|
12
|
+
maxDepth?: number;
|
|
13
|
+
/** How often a turn reports it's still running, so a client's tool timeout doesn't cut it (default 15 s). */
|
|
14
|
+
progressIntervalMs?: number;
|
|
15
|
+
/** The transport: stdio by default. */
|
|
16
|
+
transport?: Transport;
|
|
17
|
+
/** Runs the agent: the SDK's by default; another for tests. */
|
|
18
|
+
runQuery?: ChatSettings["runQuery"];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Serves an agent as an MCP server (ADR-027), so another agent (Claude Code, Claude Desktop,
|
|
22
|
+
* another one on the kit) drives it whole: its prompt, extensions and guardrails stay in its own
|
|
23
|
+
* process. One tool, `ask`; a `session_id` names a run, so a conversation goes on across calls.
|
|
24
|
+
*
|
|
25
|
+
* Autonomous mode only: nobody can answer a checkpoint here, and the calling model must never
|
|
26
|
+
* answer for the human. Resolves when the client disconnects.
|
|
27
|
+
*/
|
|
28
|
+
export declare function serveMcp(opener: SessionOpener, settings: ServeMcpSettings): Promise<void>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { getInteractionPort, setInteractionPort } from "../core/interaction.js";
|
|
6
|
+
import { MCP_DEPTH_ENV, mcpDepth } from "../core/mcpDepth.js";
|
|
7
|
+
import { createRunFolder, readRunSession } from "../core/runs.js";
|
|
8
|
+
import { createChatController } from "../chat/chatController.js";
|
|
9
|
+
import { openSession } from "../chat/runs.js";
|
|
10
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
11
|
+
const DEFAULT_PROGRESS_INTERVAL_MS = 15_000;
|
|
12
|
+
/** Sessions kept open between turns: each is a process. One closed is opened again by its `session_id`. */
|
|
13
|
+
const MAX_OPEN_SESSIONS = 4;
|
|
14
|
+
/**
|
|
15
|
+
* Serves an agent as an MCP server (ADR-027), so another agent (Claude Code, Claude Desktop,
|
|
16
|
+
* another one on the kit) drives it whole: its prompt, extensions and guardrails stay in its own
|
|
17
|
+
* process. One tool, `ask`; a `session_id` names a run, so a conversation goes on across calls.
|
|
18
|
+
*
|
|
19
|
+
* Autonomous mode only: nobody can answer a checkpoint here, and the calling model must never
|
|
20
|
+
* answer for the human. Resolves when the client disconnects.
|
|
21
|
+
*/
|
|
22
|
+
export async function serveMcp(opener, settings) {
|
|
23
|
+
const { runsDir, identity } = settings;
|
|
24
|
+
const maxDepth = settings.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
25
|
+
const interval = settings.progressIntervalMs ?? DEFAULT_PROGRESS_INTERVAL_MS;
|
|
26
|
+
const depth = mcpDepth();
|
|
27
|
+
// What this function takes over in the process, given back when it ends. Over stdio, stdout
|
|
28
|
+
// belongs to the transport: whatever else prints goes to stderr.
|
|
29
|
+
const previousDepth = process.env[MCP_DEPTH_ENV];
|
|
30
|
+
const previousPort = getInteractionPort();
|
|
31
|
+
const { log, info, debug } = console;
|
|
32
|
+
const stdio = !settings.transport;
|
|
33
|
+
process.env[MCP_DEPTH_ENV] = String(depth + 1);
|
|
34
|
+
setInteractionPort(null); // the response file alone: no keyboard here
|
|
35
|
+
if (stdio)
|
|
36
|
+
console.log = console.info = console.debug = console.error;
|
|
37
|
+
const restore = () => {
|
|
38
|
+
if (previousDepth === undefined)
|
|
39
|
+
delete process.env[MCP_DEPTH_ENV];
|
|
40
|
+
else
|
|
41
|
+
process.env[MCP_DEPTH_ENV] = previousDepth;
|
|
42
|
+
setInteractionPort(previousPort);
|
|
43
|
+
if (stdio)
|
|
44
|
+
Object.assign(console, { log, info, debug });
|
|
45
|
+
};
|
|
46
|
+
async function open(run) {
|
|
47
|
+
const opened = await openSession(opener, run);
|
|
48
|
+
if (opened.modeControl?.mode !== "autonomous")
|
|
49
|
+
throw new Error(`serveMcp() serves autonomous mode only, and this session opened in ${opened.modeControl?.mode ?? "no known mode"}: nobody can answer a checkpoint over MCP.`);
|
|
50
|
+
return opened;
|
|
51
|
+
}
|
|
52
|
+
// Opened at the start to refuse a wrong mode at once; the first new conversation uses it.
|
|
53
|
+
let spare;
|
|
54
|
+
try {
|
|
55
|
+
spare = await open(await createRunFolder(runsDir));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
restore();
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
const sessions = new Map();
|
|
62
|
+
const busy = new Set();
|
|
63
|
+
async function controllerOf(opened) {
|
|
64
|
+
return await createChatController(opened.options, {
|
|
65
|
+
sessionLogPath: path.join(opened.run.dir, "session.log"),
|
|
66
|
+
promptSuggestions: false,
|
|
67
|
+
...(opened.modeControl ? { modeControl: opened.modeControl } : {}),
|
|
68
|
+
...(opened.toolLabels ? { toolLabels: opened.toolLabels } : {}),
|
|
69
|
+
...(settings.runQuery ? { runQuery: settings.runQuery } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** The session a `session_id` names, opened again if it isn't: only a run of this agent's. */
|
|
73
|
+
async function reopen(id) {
|
|
74
|
+
const running = sessions.get(id);
|
|
75
|
+
if (running)
|
|
76
|
+
return running;
|
|
77
|
+
const dir = path.join(runsDir, id);
|
|
78
|
+
const sessionId = /^[\w-]+$/.test(id) ? await readRunSession(dir) : null;
|
|
79
|
+
if (!sessionId)
|
|
80
|
+
throw new Error(`Unknown session_id: ${id}.`);
|
|
81
|
+
return await controllerOf(await open({ dir, sessionId }));
|
|
82
|
+
}
|
|
83
|
+
function closeIdleSessions() {
|
|
84
|
+
for (const [id, controller] of sessions) {
|
|
85
|
+
if (sessions.size <= MAX_OPEN_SESSIONS)
|
|
86
|
+
return;
|
|
87
|
+
if (busy.has(id))
|
|
88
|
+
continue;
|
|
89
|
+
controller.close();
|
|
90
|
+
sessions.delete(id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const server = new McpServer({ name: identity.name, version: identity.version ?? "0.0.0" });
|
|
94
|
+
server.registerTool("ask", {
|
|
95
|
+
description: `Ask ${identity.name}${identity.description ? `, ${identity.description},` : ""} to do something, and get its reply. Without session_id it starts a new conversation; with the session_id of an earlier reply it goes on with that one.`,
|
|
96
|
+
inputSchema: { message: z.string().min(1).describe("What to ask, in plain words."), session_id: z.string().optional().describe("The session_id of an earlier reply, to go on with that conversation.") },
|
|
97
|
+
}, async ({ message, session_id: requested }, extra) => {
|
|
98
|
+
if (depth >= maxDepth)
|
|
99
|
+
throw new Error(`Too many agents chained over MCP (the limit is ${maxDepth}).`);
|
|
100
|
+
if (requested !== undefined && busy.has(requested))
|
|
101
|
+
throw new Error(`Session ${requested} is busy with another turn; wait for it to end.`);
|
|
102
|
+
if (requested !== undefined)
|
|
103
|
+
busy.add(requested);
|
|
104
|
+
let id = requested;
|
|
105
|
+
try {
|
|
106
|
+
let controller;
|
|
107
|
+
if (requested !== undefined)
|
|
108
|
+
controller = await reopen(requested);
|
|
109
|
+
else {
|
|
110
|
+
const opened = spare ?? (await open(await createRunFolder(runsDir)));
|
|
111
|
+
spare = null;
|
|
112
|
+
id = path.basename(opened.run.dir);
|
|
113
|
+
busy.add(id);
|
|
114
|
+
controller = await controllerOf(opened);
|
|
115
|
+
}
|
|
116
|
+
// Last in the map: the least recently used are closed first.
|
|
117
|
+
sessions.delete(id);
|
|
118
|
+
sessions.set(id, controller);
|
|
119
|
+
const token = extra._meta?.progressToken;
|
|
120
|
+
let progress = 0;
|
|
121
|
+
let activity;
|
|
122
|
+
const report = () => {
|
|
123
|
+
if (token === undefined)
|
|
124
|
+
return;
|
|
125
|
+
void extra.sendNotification({ method: "notifications/progress", params: { progressToken: token, progress: ++progress, ...(activity ? { message: activity } : {}) } }).catch(() => undefined);
|
|
126
|
+
};
|
|
127
|
+
let failure = null;
|
|
128
|
+
const stopListening = controller.onEvent((chat) => {
|
|
129
|
+
if (chat.type !== "agent")
|
|
130
|
+
return;
|
|
131
|
+
const event = chat.event;
|
|
132
|
+
if (event.type === "action" || event.type === "subagent-action") {
|
|
133
|
+
activity = controller.formatAction(event.toolName, event.input);
|
|
134
|
+
report();
|
|
135
|
+
}
|
|
136
|
+
else if (event.type === "turn-end" && event.failed)
|
|
137
|
+
failure = event.errorText || "The turn failed.";
|
|
138
|
+
});
|
|
139
|
+
const heartbeat = setInterval(report, interval);
|
|
140
|
+
const interrupt = () => controller.interrupt();
|
|
141
|
+
extra.signal.addEventListener("abort", interrupt, { once: true });
|
|
142
|
+
try {
|
|
143
|
+
await controller.turn(message);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
clearInterval(heartbeat);
|
|
147
|
+
stopListening();
|
|
148
|
+
extra.signal.removeEventListener("abort", interrupt);
|
|
149
|
+
}
|
|
150
|
+
if (failure)
|
|
151
|
+
throw new Error(failure);
|
|
152
|
+
return { content: [{ type: "text", text: controller.lastReply() }, { type: "text", text: `session_id: ${id}` }] };
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (id !== undefined)
|
|
156
|
+
busy.delete(id);
|
|
157
|
+
closeIdleSessions();
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return await new Promise((resolve, reject) => {
|
|
161
|
+
server.server.onclose = () => {
|
|
162
|
+
for (const controller of sessions.values())
|
|
163
|
+
controller.close();
|
|
164
|
+
sessions.clear();
|
|
165
|
+
restore();
|
|
166
|
+
resolve();
|
|
167
|
+
};
|
|
168
|
+
const transport = settings.transport ?? new StdioServerTransport();
|
|
169
|
+
// The stdio transport doesn't see its client leave: stdin ending is how it does.
|
|
170
|
+
if (stdio)
|
|
171
|
+
process.stdin.once("end", () => void server.close());
|
|
172
|
+
server.connect(transport).catch((error) => {
|
|
173
|
+
restore();
|
|
174
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falkenslab/agent-kit",
|
|
3
|
-
"version": "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",
|
|
@@ -30,6 +30,17 @@
|
|
|
30
30
|
},
|
|
31
31
|
"main": "./dist/index.js",
|
|
32
32
|
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./mcp": {
|
|
39
|
+
"types": "./dist/mcp/index.d.ts",
|
|
40
|
+
"default": "./dist/mcp/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
33
44
|
"files": [
|
|
34
45
|
"dist",
|
|
35
46
|
"assets",
|
|
@@ -52,6 +63,7 @@
|
|
|
52
63
|
"@anthropic-ai/claude-agent-sdk": "^0.3.300",
|
|
53
64
|
"@inkjs/ui": "^2.0.0",
|
|
54
65
|
"@inquirer/prompts": "^8.5.2",
|
|
66
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
55
67
|
"@types/react": "^19.3.0",
|
|
56
68
|
"ink": "^6.8.0",
|
|
57
69
|
"isomorphic-git": "^1.44.5",
|