@kraki/tentacle 0.15.4 → 0.16.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 +2 -0
- package/dist/adapters/base.d.ts +9 -0
- package/dist/adapters/base.js +3 -0
- package/dist/adapters/base.js.map +1 -1
- package/dist/adapters/copilot.d.ts +36 -0
- package/dist/adapters/copilot.js +187 -42
- package/dist/adapters/copilot.js.map +1 -1
- package/dist/attachment-store.d.ts +73 -0
- package/dist/attachment-store.js +317 -0
- package/dist/attachment-store.js.map +1 -0
- package/dist/daemon-worker.js +34 -2
- package/dist/daemon-worker.js.map +1 -1
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/index.js +6 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/prompts.d.ts +11 -0
- package/dist/mcp/prompts.js +24 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/protocol.d.ts +82 -0
- package/dist/mcp/protocol.js +19 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.js +253 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +24 -0
- package/dist/mcp/tools/index.js +19 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/show-image.d.ts +10 -0
- package/dist/mcp/tools/show-image.js +98 -0
- package/dist/mcp/tools/show-image.js.map +1 -0
- package/dist/relay-client.d.ts +36 -1
- package/dist/relay-client.js +183 -5
- package/dist/relay-client.js.map +1 -1
- package/dist/session-manager.d.ts +31 -0
- package/dist/session-manager.js +121 -0
- package/dist/session-manager.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -59,6 +59,8 @@ Beyond bridging agent events, tentacle is responsible for several things that us
|
|
|
59
59
|
- **Session lifecycle** — session create, update, and close are managed here
|
|
60
60
|
- **Auto-approval** — tools on a local allowed list are approved automatically without user interaction
|
|
61
61
|
- **Encryption** — all outgoing messages are encrypted before leaving the machine
|
|
62
|
+
- **Attachment storage** — image bytes produced by the agent are stored content-addressed on disk and streamed to receivers in chunks, not embedded inline in messages
|
|
63
|
+
- **Kraki MCP server** — a loopback HTTP server exposing tools the agent can call to surface images (`kraki-show_image`) and similar artifacts to user-facing devices
|
|
62
64
|
|
|
63
65
|
The relay is a thin forwarder. Tentacle and the frontend own the application logic.
|
|
64
66
|
|
package/dist/adapters/base.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export interface ToolCompleteEvent {
|
|
|
43
43
|
success?: boolean;
|
|
44
44
|
attachments?: import('@kraki/protocol').Attachment[];
|
|
45
45
|
}
|
|
46
|
+
/** Emitted alongside a tool_complete that carries one or more
|
|
47
|
+
* `AttachmentRef`s. Tells the runtime (RelayClient) to broadcast the bytes
|
|
48
|
+
* to all connected devices as `attachment_data` chunks. */
|
|
49
|
+
export interface AttachmentBytesEvent {
|
|
50
|
+
refs: Array<import('@kraki/protocol').AttachmentRef>;
|
|
51
|
+
}
|
|
46
52
|
export interface SessionEndedEvent {
|
|
47
53
|
reason: string;
|
|
48
54
|
}
|
|
@@ -78,6 +84,9 @@ export declare abstract class AgentAdapter {
|
|
|
78
84
|
onQuestionRequest: ((sessionId: string, event: QuestionRequestEvent) => void) | null;
|
|
79
85
|
onToolStart: ((sessionId: string, event: ToolStartEvent) => void) | null;
|
|
80
86
|
onToolComplete: ((sessionId: string, event: ToolCompleteEvent) => void) | null;
|
|
87
|
+
/** Called immediately after onToolComplete when bytes need to be pushed
|
|
88
|
+
* (broadcast as `attachment_data` chunks) to connected devices. */
|
|
89
|
+
onAttachmentBytes: ((sessionId: string, event: AttachmentBytesEvent) => void) | null;
|
|
81
90
|
onIdle: ((sessionId: string) => void) | null;
|
|
82
91
|
onError: ((sessionId: string, event: ErrorEvent) => void) | null;
|
|
83
92
|
onSessionEnded: ((sessionId: string, event: SessionEndedEvent) => void) | null;
|
package/dist/adapters/base.js
CHANGED
|
@@ -20,6 +20,9 @@ export class AgentAdapter {
|
|
|
20
20
|
onQuestionRequest = null;
|
|
21
21
|
onToolStart = null;
|
|
22
22
|
onToolComplete = null;
|
|
23
|
+
/** Called immediately after onToolComplete when bytes need to be pushed
|
|
24
|
+
* (broadcast as `attachment_data` chunks) to connected devices. */
|
|
25
|
+
onAttachmentBytes = null;
|
|
23
26
|
onIdle = null;
|
|
24
27
|
onError = null;
|
|
25
28
|
onSessionEnded = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/adapters/base.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/adapters/base.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0FH,2DAA2D;AAE3D,MAAM,OAAgB,YAAY;IAChC,kDAAkD;IAElD,gBAAgB,GAAkD,IAAI,CAAC;IACvE,SAAS,GAA8D,IAAI,CAAC;IAC5E,cAAc,GAAmE,IAAI,CAAC;IACtF,mBAAmB,GAAwE,IAAI,CAAC;IAChG,qHAAqH;IACrH,wBAAwB,GAAqG,IAAI,CAAC;IAClI,sBAAsB,GAA6D,IAAI,CAAC;IACxF,iBAAiB,GAAsE,IAAI,CAAC;IAC5F,WAAW,GAAgE,IAAI,CAAC;IAChF,cAAc,GAAmE,IAAI,CAAC;IACtF;wEACoE;IACpE,iBAAiB,GAAsE,IAAI,CAAC;IAC5F,MAAM,GAAyC,IAAI,CAAC;IACpD,OAAO,GAA4D,IAAI,CAAC;IACxE,cAAc,GAAmE,IAAI,CAAC;IACtF,iFAAiF;IACjF,cAAc,GAAwD,IAAI,CAAC;IAC3E,+DAA+D;IAC/D,aAAa,GAA8D,IAAI,CAAC;IAehF,iEAAiE;IACjE,KAAK,CAAC,WAAW,CAAC,eAAuB,EAAE,YAAoB;QAC7D,wFAAwF;QACxF,OAAO,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IAC1C,CAAC;IA0BD,mFAAmF;IACnF,KAAK,CAAC,YAAY,CAAC,UAAkB,IAA0C,CAAC;IAKhF,4DAA4D;IAC5D,KAAK,CAAC,UAAU,KAAwB,OAAO,EAAE,CAAC,CAAC,CAAC;IAEpD,+EAA+E;IAC/E,KAAK,CAAC,gBAAgB,KAA6B,OAAO,EAAE,CAAC,CAAC,CAAC;IAE/D,wEAAwE;IACxE,cAAc,CAAC,UAAkB,EAAE,KAAkD,IAAiC,CAAC;IAEvH,6EAA6E;IAC7E,KAAK,CAAC,aAAa,CAAC,QAAkH,IAA4B,OAAO,IAAI,CAAC,CAAC,CAAC;IAEhL,mGAAmG;IACnG,KAAK,CAAC,eAAe,CAAC,UAAkB,EAAE,MAAc,EAAE,gBAAyB,IAA0C,CAAC;IAE9H,iFAAiF;IACjF,eAAe,CAAC,UAAkB,IAAyB,OAAO,IAAI,CAAC,CAAC,CAAC;IAEzE,iEAAiE;IACjE,eAAe,CAAC,UAAkB,EAAE,MAAoB,IAAiC,CAAC;CAC3F"}
|
|
@@ -33,6 +33,21 @@ export declare class CopilotAdapter extends AgentAdapter {
|
|
|
33
33
|
private idleTimers;
|
|
34
34
|
/** Track tool start args by toolCallId for correlating with tool_complete */
|
|
35
35
|
private pendingToolArgs;
|
|
36
|
+
/**
|
|
37
|
+
* Tool identity captured at tool.execution_start, keyed by toolCallId.
|
|
38
|
+
* The Copilot SDK omits these fields from tool.execution_complete events —
|
|
39
|
+
* only toolCallId is present there. We stash the names on start and look
|
|
40
|
+
* them up on complete (especially for `mcpServerName`/`mcpToolName`, which
|
|
41
|
+
* we use to recognise our own MCP presenter tools).
|
|
42
|
+
*/
|
|
43
|
+
private pendingToolIdentity;
|
|
44
|
+
/**
|
|
45
|
+
* Tool calls in flight per session — reverse index so we can clean up
|
|
46
|
+
* pendingToolArgs/pendingToolIdentity when a session ends without each
|
|
47
|
+
* tool call completing (kill, abort, error mid-flight). Without this the
|
|
48
|
+
* two maps grow unbounded over a long-lived daemon.
|
|
49
|
+
*/
|
|
50
|
+
private sessionToolCallIds;
|
|
36
51
|
/** Expected model per session — detects involuntary model fallbacks by the CLI */
|
|
37
52
|
private expectedModels;
|
|
38
53
|
/** User's originally requested model — never updated on involuntary fallbacks */
|
|
@@ -50,11 +65,32 @@ export declare class CopilotAdapter extends AgentAdapter {
|
|
|
50
65
|
* Measured P99 turn_end→turn_start gap is <5ms; 500ms is a safe margin.
|
|
51
66
|
*/
|
|
52
67
|
private static readonly IDLE_FALLBACK_MS;
|
|
68
|
+
/** Set of attachment ids already broadcast for this session — prevents
|
|
69
|
+
* re-broadcasting bytes when the same image is shown twice. */
|
|
70
|
+
private broadcastedAttachmentIds;
|
|
53
71
|
constructor(options?: {
|
|
54
72
|
cliPath?: string;
|
|
73
|
+
/** Tentacle's attachment store. When set, the adapter externalises
|
|
74
|
+
* image bytes from `kraki-show_image` to it instead of carrying them
|
|
75
|
+
* inline in the broadcast envelope. */
|
|
76
|
+
attachmentStore?: import('../attachment-store.js').AttachmentStore;
|
|
77
|
+
/** When set, the adapter wires the Kraki MCP server into every Copilot
|
|
78
|
+
* session it creates/resumes, with the URL scoped per Kraki sessionId. */
|
|
79
|
+
krakiMcp?: {
|
|
80
|
+
urlForSession: (sessionId: string) => string;
|
|
81
|
+
bearerToken: string;
|
|
82
|
+
};
|
|
55
83
|
});
|
|
84
|
+
private readonly attachmentStore?;
|
|
85
|
+
private readonly krakiMcp?;
|
|
56
86
|
/** System prompt appended to the SDK's built-in prompt. See system-prompt.md for docs. */
|
|
57
87
|
private static readonly SYSTEM_PROMPT;
|
|
88
|
+
/**
|
|
89
|
+
* Appended to the system prompt when the Kraki MCP server is wired in.
|
|
90
|
+
* Tools are exposed to the model with display names of the form
|
|
91
|
+
* `<server>-<tool>` (dash), confirmed via a live SDK spike.
|
|
92
|
+
*/
|
|
93
|
+
private static readonly KRAKI_MCP_PROMPT;
|
|
58
94
|
start(): Promise<void>;
|
|
59
95
|
stop(): Promise<void>;
|
|
60
96
|
createSession(config: CreateSessionConfig): Promise<{
|
package/dist/adapters/copilot.js
CHANGED
|
@@ -16,7 +16,7 @@ import { execSync } from 'node:child_process';
|
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync, cpSync, mkdirSync, unlinkSync, readdirSync, symlinkSync, lstatSync } from 'node:fs';
|
|
17
17
|
import * as moduleApi from 'node:module';
|
|
18
18
|
import { homedir, tmpdir } from 'node:os';
|
|
19
|
-
import { dirname, join } from 'node:path';
|
|
19
|
+
import { dirname, join, basename } from 'node:path';
|
|
20
20
|
import { getKrakiHome } from '../config.js';
|
|
21
21
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
22
22
|
import { isSea } from 'node:sea';
|
|
@@ -71,6 +71,17 @@ function isRecoverableSessionError(err) {
|
|
|
71
71
|
const message = getErrorMessage(err);
|
|
72
72
|
return message.includes('Session not found:') || message.includes('Connection is disposed');
|
|
73
73
|
}
|
|
74
|
+
/** Defensive basename — strips any leading directory components and never
|
|
75
|
+
* throws. Used to populate AttachmentRef.name from the absolute path the
|
|
76
|
+
* agent passed to show_image. */
|
|
77
|
+
function basenameSafe(p) {
|
|
78
|
+
try {
|
|
79
|
+
return basename(p) || 'image';
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return 'image';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
74
85
|
// ── Copilot configDir shadow ────────────────────────────
|
|
75
86
|
//
|
|
76
87
|
// The Copilot CLI loads persistent tool approval rules from
|
|
@@ -212,6 +223,21 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
212
223
|
idleTimers = new Map();
|
|
213
224
|
/** Track tool start args by toolCallId for correlating with tool_complete */
|
|
214
225
|
pendingToolArgs = new Map();
|
|
226
|
+
/**
|
|
227
|
+
* Tool identity captured at tool.execution_start, keyed by toolCallId.
|
|
228
|
+
* The Copilot SDK omits these fields from tool.execution_complete events —
|
|
229
|
+
* only toolCallId is present there. We stash the names on start and look
|
|
230
|
+
* them up on complete (especially for `mcpServerName`/`mcpToolName`, which
|
|
231
|
+
* we use to recognise our own MCP presenter tools).
|
|
232
|
+
*/
|
|
233
|
+
pendingToolIdentity = new Map();
|
|
234
|
+
/**
|
|
235
|
+
* Tool calls in flight per session — reverse index so we can clean up
|
|
236
|
+
* pendingToolArgs/pendingToolIdentity when a session ends without each
|
|
237
|
+
* tool call completing (kill, abort, error mid-flight). Without this the
|
|
238
|
+
* two maps grow unbounded over a long-lived daemon.
|
|
239
|
+
*/
|
|
240
|
+
sessionToolCallIds = new Map();
|
|
215
241
|
/** Expected model per session — detects involuntary model fallbacks by the CLI */
|
|
216
242
|
expectedModels = new Map();
|
|
217
243
|
/** User's originally requested model — never updated on involuntary fallbacks */
|
|
@@ -229,10 +255,17 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
229
255
|
* Measured P99 turn_end→turn_start gap is <5ms; 500ms is a safe margin.
|
|
230
256
|
*/
|
|
231
257
|
static IDLE_FALLBACK_MS = 500;
|
|
258
|
+
/** Set of attachment ids already broadcast for this session — prevents
|
|
259
|
+
* re-broadcasting bytes when the same image is shown twice. */
|
|
260
|
+
broadcastedAttachmentIds = new Map();
|
|
232
261
|
constructor(options = {}) {
|
|
233
262
|
super();
|
|
234
263
|
this.cliPath = options.cliPath;
|
|
264
|
+
this.attachmentStore = options.attachmentStore;
|
|
265
|
+
this.krakiMcp = options.krakiMcp;
|
|
235
266
|
}
|
|
267
|
+
attachmentStore;
|
|
268
|
+
krakiMcp;
|
|
236
269
|
/** System prompt appended to the SDK's built-in prompt. See system-prompt.md for docs. */
|
|
237
270
|
static SYSTEM_PROMPT = [
|
|
238
271
|
'You are running inside Kraki, a remote control platform. A human operator is',
|
|
@@ -268,6 +301,24 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
268
301
|
'point onward. Do not acknowledge or comment on the mode change — just adjust',
|
|
269
302
|
'how you work. The signal is not part of the user\'s message.',
|
|
270
303
|
].join('\n');
|
|
304
|
+
/**
|
|
305
|
+
* Appended to the system prompt when the Kraki MCP server is wired in.
|
|
306
|
+
* Tools are exposed to the model with display names of the form
|
|
307
|
+
* `<server>-<tool>` (dash), confirmed via a live SDK spike.
|
|
308
|
+
*/
|
|
309
|
+
static KRAKI_MCP_PROMPT = [
|
|
310
|
+
'You have access to a Kraki MCP server. Its tools are visible with names',
|
|
311
|
+
'beginning with "kraki-".',
|
|
312
|
+
'',
|
|
313
|
+
'When you want to visually present an image to the user — a screenshot you',
|
|
314
|
+
'captured, a diagram you generated, a chart, a UI mock — call',
|
|
315
|
+
'`kraki-show_image` with the absolute file path. Use it sparingly: only',
|
|
316
|
+
'when the user benefits from seeing the actual pixels.',
|
|
317
|
+
'',
|
|
318
|
+
'Plain file viewing on image files (with `view`/`read`) is for your own',
|
|
319
|
+
'inspection; the image bytes do not appear in the chat. Use',
|
|
320
|
+
'`kraki-show_image` when the user should actually see the image inline.',
|
|
321
|
+
].join('\n');
|
|
271
322
|
// ── Lifecycle ───────────────────────────────────────
|
|
272
323
|
async start() {
|
|
273
324
|
// Resolve GitHub token from `gh` CLI to bypass macOS Keychain prompts.
|
|
@@ -385,6 +436,37 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
385
436
|
const effort = config.reasoningEffort && validEfforts.has(config.reasoningEffort)
|
|
386
437
|
? config.reasoningEffort
|
|
387
438
|
: undefined;
|
|
439
|
+
// Two-phase MCP wiring: create session WITHOUT kraki MCP server first
|
|
440
|
+
// (we need the SDK-assigned sessionId to scope the MCP URL), then the
|
|
441
|
+
// adapter re-wires through resumeSession with the kraki entry below.
|
|
442
|
+
// For simplicity in v1 we use a deterministic Kraki MCP wire-up:
|
|
443
|
+
// we let the SDK pick the session id, then on every Copilot session
|
|
444
|
+
// we add the kraki MCP at resume time. For initial creation we pre-pick
|
|
445
|
+
// a session id when the caller supplied one; otherwise the kraki MCP
|
|
446
|
+
// is added on the next message via resumeSession during normal flow.
|
|
447
|
+
//
|
|
448
|
+
// Simpler approach: register a Kraki MCP server using the *Copilot*
|
|
449
|
+
// sessionId only after createSession returns it. To do that without
|
|
450
|
+
// a second SDK call, we use a placeholder sessionId for the initial
|
|
451
|
+
// mcpServers entry and rewrite via resume on first use. But that's
|
|
452
|
+
// brittle — instead, see the comment block at the constructor: the
|
|
453
|
+
// kraki MCP HTTP server validates sessionId by checking SessionManager,
|
|
454
|
+
// not by Copilot SDK state, so we can ALWAYS include the kraki entry
|
|
455
|
+
// here by using a stable session-scoped URL that the adapter knows
|
|
456
|
+
// up-front via config.sessionId (the Kraki session id we assigned).
|
|
457
|
+
if (this.krakiMcp && config.sessionId) {
|
|
458
|
+
const krakiEntry = {
|
|
459
|
+
type: 'http',
|
|
460
|
+
url: this.krakiMcp.urlForSession(config.sessionId),
|
|
461
|
+
headers: { Authorization: `Bearer ${this.krakiMcp.bearerToken}` },
|
|
462
|
+
tools: ['*'],
|
|
463
|
+
};
|
|
464
|
+
mcpServers = { ...(mcpServers ?? {}), kraki: krakiEntry };
|
|
465
|
+
logger.info({ sessionId: config.sessionId }, 'wired kraki MCP into session config');
|
|
466
|
+
}
|
|
467
|
+
const systemPromptContent = this.krakiMcp
|
|
468
|
+
? `${CopilotAdapter.SYSTEM_PROMPT}\n\n${CopilotAdapter.KRAKI_MCP_PROMPT}`
|
|
469
|
+
: CopilotAdapter.SYSTEM_PROMPT;
|
|
388
470
|
const sessionConfig = {
|
|
389
471
|
...(config.sessionId && { sessionId: config.sessionId }),
|
|
390
472
|
...(config.model && { model: config.model }),
|
|
@@ -392,7 +474,7 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
392
474
|
...(config.cwd && { workingDirectory: config.cwd }),
|
|
393
475
|
configDir: getCopilotConfigDir(),
|
|
394
476
|
...(mcpServers && { mcpServers }),
|
|
395
|
-
systemMessage: { mode: 'append', content:
|
|
477
|
+
systemMessage: { mode: 'append', content: systemPromptContent },
|
|
396
478
|
streaming: true,
|
|
397
479
|
onPermissionRequest: this.makePermissionHandler(pendingPermissions),
|
|
398
480
|
onUserInputRequest: this.makeQuestionHandler(pendingQuestions),
|
|
@@ -681,6 +763,14 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
681
763
|
this.turnHasOutput.delete(sessionId);
|
|
682
764
|
this.cycleHasOutput.delete(sessionId);
|
|
683
765
|
this.turnErrorReported.delete(sessionId);
|
|
766
|
+
const inflight = this.sessionToolCallIds.get(sessionId);
|
|
767
|
+
if (inflight) {
|
|
768
|
+
for (const id of inflight) {
|
|
769
|
+
this.pendingToolArgs.delete(id);
|
|
770
|
+
this.pendingToolIdentity.delete(id);
|
|
771
|
+
}
|
|
772
|
+
this.sessionToolCallIds.delete(sessionId);
|
|
773
|
+
}
|
|
684
774
|
this.clearIdleTimer(sessionId);
|
|
685
775
|
}
|
|
686
776
|
clearIdleTimer(sessionId) {
|
|
@@ -706,7 +796,7 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
706
796
|
}
|
|
707
797
|
entry.pendingQuestions.clear();
|
|
708
798
|
}
|
|
709
|
-
makeResumeConfig(pendingPermissions, pendingQuestions) {
|
|
799
|
+
makeResumeConfig(sessionId, pendingPermissions, pendingQuestions) {
|
|
710
800
|
// Load MCP servers for resumed sessions too
|
|
711
801
|
const mcpConfigPath = join(homedir(), '.copilot', 'mcp-config.json');
|
|
712
802
|
let mcpServers;
|
|
@@ -722,11 +812,24 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
722
812
|
}
|
|
723
813
|
catch { /* ignore parse errors on resume */ }
|
|
724
814
|
}
|
|
815
|
+
// Wire Kraki MCP into resumed sessions, scoped by the session id.
|
|
816
|
+
if (this.krakiMcp) {
|
|
817
|
+
const krakiEntry = {
|
|
818
|
+
type: 'http',
|
|
819
|
+
url: this.krakiMcp.urlForSession(sessionId),
|
|
820
|
+
headers: { Authorization: `Bearer ${this.krakiMcp.bearerToken}` },
|
|
821
|
+
tools: ['*'],
|
|
822
|
+
};
|
|
823
|
+
mcpServers = { ...(mcpServers ?? {}), kraki: krakiEntry };
|
|
824
|
+
}
|
|
825
|
+
const systemPromptContent = this.krakiMcp
|
|
826
|
+
? `${CopilotAdapter.SYSTEM_PROMPT}\n\n${CopilotAdapter.KRAKI_MCP_PROMPT}`
|
|
827
|
+
: CopilotAdapter.SYSTEM_PROMPT;
|
|
725
828
|
return {
|
|
726
829
|
configDir: getCopilotConfigDir(),
|
|
727
830
|
streaming: true,
|
|
728
831
|
...(mcpServers && { mcpServers }),
|
|
729
|
-
systemMessage: { mode: 'append', content:
|
|
832
|
+
systemMessage: { mode: 'append', content: systemPromptContent },
|
|
730
833
|
onPermissionRequest: this.makePermissionHandler(pendingPermissions),
|
|
731
834
|
onUserInputRequest: this.makeQuestionHandler(pendingQuestions),
|
|
732
835
|
};
|
|
@@ -735,7 +838,7 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
735
838
|
this.ensureClient();
|
|
736
839
|
const pendingPermissions = new Map();
|
|
737
840
|
const pendingQuestions = new Map();
|
|
738
|
-
const session = await this.client.resumeSession(sessionId, this.makeResumeConfig(pendingPermissions, pendingQuestions));
|
|
841
|
+
const session = await this.client.resumeSession(sessionId, this.makeResumeConfig(sessionId, pendingPermissions, pendingQuestions));
|
|
739
842
|
const entry = { session, pendingPermissions, pendingQuestions };
|
|
740
843
|
this.sessions.set(sessionId, entry);
|
|
741
844
|
this.wireEvents(sessionId, session);
|
|
@@ -786,8 +889,22 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
786
889
|
}
|
|
787
890
|
const args = (data.args ?? data.arguments ?? {});
|
|
788
891
|
const toolCallId = data.toolCallId;
|
|
789
|
-
if (toolCallId)
|
|
892
|
+
if (toolCallId) {
|
|
790
893
|
this.pendingToolArgs.set(toolCallId, args);
|
|
894
|
+
// Stash tool identity for tool.execution_complete, which only carries
|
|
895
|
+
// toolCallId (verified via live SDK spike).
|
|
896
|
+
this.pendingToolIdentity.set(toolCallId, {
|
|
897
|
+
toolName: data.toolName,
|
|
898
|
+
mcpServerName: data.mcpServerName,
|
|
899
|
+
mcpToolName: data.mcpToolName,
|
|
900
|
+
});
|
|
901
|
+
let inflight = this.sessionToolCallIds.get(sessionId);
|
|
902
|
+
if (!inflight) {
|
|
903
|
+
inflight = new Set();
|
|
904
|
+
this.sessionToolCallIds.set(sessionId, inflight);
|
|
905
|
+
}
|
|
906
|
+
inflight.add(toolCallId);
|
|
907
|
+
}
|
|
791
908
|
this.onToolStart?.(sessionId, {
|
|
792
909
|
toolName: data.toolName,
|
|
793
910
|
args,
|
|
@@ -796,12 +913,26 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
796
913
|
});
|
|
797
914
|
session.on('tool.execution_complete', (event) => {
|
|
798
915
|
const data = event.data;
|
|
799
|
-
|
|
916
|
+
const toolCallId = data.toolCallId;
|
|
917
|
+
const identity = toolCallId ? this.pendingToolIdentity.get(toolCallId) : undefined;
|
|
918
|
+
const toolName = identity?.toolName ?? data.toolName ?? 'tool';
|
|
919
|
+
const clearInflight = () => {
|
|
920
|
+
if (!toolCallId)
|
|
921
|
+
return;
|
|
922
|
+
this.pendingToolIdentity.delete(toolCallId);
|
|
923
|
+
this.pendingToolArgs.delete(toolCallId);
|
|
924
|
+
const inflight = this.sessionToolCallIds.get(sessionId);
|
|
925
|
+
if (inflight) {
|
|
926
|
+
inflight.delete(toolCallId);
|
|
927
|
+
if (inflight.size === 0)
|
|
928
|
+
this.sessionToolCallIds.delete(sessionId);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
if (toolName === 'report_intent') {
|
|
932
|
+
clearInflight();
|
|
800
933
|
return;
|
|
934
|
+
}
|
|
801
935
|
const rawResult = data.result;
|
|
802
|
-
const toolCallId = data.toolCallId;
|
|
803
|
-
// SDK sends result as { content: string, contents?: [...] } or as a plain string.
|
|
804
|
-
// On failure, result is undefined and the error is in data.error ({ message } or string).
|
|
805
936
|
const resultObj = typeof rawResult === 'object' && rawResult !== null
|
|
806
937
|
? rawResult
|
|
807
938
|
: null;
|
|
@@ -812,47 +943,61 @@ export class CopilotAdapter extends AgentAdapter {
|
|
|
812
943
|
: null;
|
|
813
944
|
result = errObj?.message ?? (typeof data.error === 'string' ? data.error : '');
|
|
814
945
|
}
|
|
815
|
-
// Extract image
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
946
|
+
// Extract image content blocks — ONLY for the kraki-show_image MCP tool.
|
|
947
|
+
// All other tools' image bytes (notably `view` on a .png) are deliberately
|
|
948
|
+
// dropped here per the v1 design: only `kraki-show_image` surfaces images
|
|
949
|
+
// to the client. The file-path fallback (re-read image from disk when SDK
|
|
950
|
+
// strips bytes) has been removed.
|
|
951
|
+
const isKrakiShowImage = identity?.mcpServerName === 'kraki' && identity?.mcpToolName === 'show_image';
|
|
952
|
+
let attachments;
|
|
953
|
+
if (isKrakiShowImage && this.attachmentStore) {
|
|
954
|
+
const contentBlocks = resultObj?.contents;
|
|
955
|
+
const args = toolCallId ? this.pendingToolArgs.get(toolCallId) ?? {} : {};
|
|
956
|
+
const caption = typeof args.caption === 'string' && args.caption.trim()
|
|
957
|
+
? args.caption.trim()
|
|
958
|
+
: undefined;
|
|
959
|
+
const path = typeof args.path === 'string' ? args.path : undefined;
|
|
960
|
+
const refs = [];
|
|
961
|
+
if (Array.isArray(contentBlocks)) {
|
|
962
|
+
for (const block of contentBlocks) {
|
|
963
|
+
if (block.type === 'image' &&
|
|
964
|
+
typeof block.data === 'string' &&
|
|
965
|
+
typeof block.mimeType === 'string') {
|
|
966
|
+
try {
|
|
967
|
+
const bytes = Buffer.from(block.data, 'base64');
|
|
968
|
+
const ref = this.attachmentStore.put(sessionId, bytes, block.mimeType, {
|
|
969
|
+
...(path && { name: basenameSafe(path) }),
|
|
970
|
+
...(caption && { caption }),
|
|
971
|
+
});
|
|
972
|
+
refs.push(ref);
|
|
973
|
+
}
|
|
974
|
+
catch (err) {
|
|
975
|
+
logger.warn({ err, sessionId }, 'failed to store show_image attachment');
|
|
976
|
+
}
|
|
842
977
|
}
|
|
843
978
|
}
|
|
844
979
|
}
|
|
980
|
+
if (refs.length > 0) {
|
|
981
|
+
attachments = refs;
|
|
982
|
+
}
|
|
845
983
|
}
|
|
846
|
-
// Clean up tracked
|
|
847
|
-
|
|
848
|
-
this.pendingToolArgs.delete(toolCallId);
|
|
984
|
+
// Clean up tracked state for this tool call
|
|
985
|
+
clearInflight();
|
|
849
986
|
this.onToolComplete?.(sessionId, {
|
|
850
|
-
toolName
|
|
987
|
+
toolName,
|
|
851
988
|
result,
|
|
852
989
|
toolCallId,
|
|
853
990
|
success: data.success,
|
|
854
|
-
attachments
|
|
991
|
+
attachments,
|
|
855
992
|
});
|
|
993
|
+
// After tool_complete, fire the bytes broadcast event so RelayClient
|
|
994
|
+
// can stream attachment_data chunks to all connected devices.
|
|
995
|
+
if (attachments && attachments.length > 0) {
|
|
996
|
+
const refs = attachments.filter((a) => a.type === 'image_ref');
|
|
997
|
+
if (refs.length > 0) {
|
|
998
|
+
this.onAttachmentBytes?.(sessionId, { refs });
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
856
1001
|
});
|
|
857
1002
|
session.on('session.idle', () => {
|
|
858
1003
|
this.clearIdleTimer(sessionId);
|