@letta-ai/letta-agent-sdk 0.3.2 → 0.3.3
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/AGENTS.md +47 -0
- package/README.md +17 -0
- package/dist/client-entry.js +800 -732
- package/dist/client-entry.js.map +8 -5
- package/dist/cloud-sandbox.d.ts +33 -0
- package/dist/cloud-sandbox.d.ts.map +1 -0
- package/dist/cloud-session.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +799 -732
- package/dist/index.js.map +9 -6
- package/dist/remote-client-session-core.d.ts +6 -93
- package/dist/remote-client-session-core.d.ts.map +1 -1
- package/dist/remote-session-protocol.d.ts +130 -0
- package/dist/remote-session-protocol.d.ts.map +1 -0
- package/dist/remote-turn-coordinator.d.ts +49 -0
- package/dist/remote-turn-coordinator.d.ts.map +1 -0
- package/dist/types.d.ts +2 -20
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/app-server-management.ts +641 -0
- package/src/app-server-session.ts +948 -0
- package/src/cli-resolver.ts +46 -0
- package/src/client-base.ts +482 -0
- package/src/client-entry.ts +31 -0
- package/src/client.ts +138 -0
- package/src/cloud-management.ts +360 -0
- package/src/cloud-sandbox.ts +117 -0
- package/src/cloud-session.ts +1313 -0
- package/src/index.ts +440 -0
- package/src/interactiveToolPolicy.ts +62 -0
- package/src/local-app-server-session.ts +39 -0
- package/src/local-app-server.ts +137 -0
- package/src/management-types.ts +133 -0
- package/src/management.ts +206 -0
- package/src/protocol.ts +249 -0
- package/src/remote-client-session-core.ts +786 -0
- package/src/remote-session-protocol.ts +660 -0
- package/src/remote-turn-coordinator.ts +505 -0
- package/src/remote.ts +177 -0
- package/src/repositories.ts +340 -0
- package/src/request-ids.ts +33 -0
- package/src/session.ts +1638 -0
- package/src/stream-events.ts +88 -0
- package/src/tool-helpers.ts +147 -0
- package/src/transport.ts +484 -0
- package/src/types.ts +1328 -0
- package/src/validation.ts +223 -0
- package/src/websocket.ts +22 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAppServerClient,
|
|
3
|
+
type AppServerClient,
|
|
4
|
+
type AppServerExternalToolCallHandler,
|
|
5
|
+
type AppServerSocketConstructor,
|
|
6
|
+
} from "@letta-ai/letta-code/app-server-client";
|
|
7
|
+
import {
|
|
8
|
+
buildCreateAgentRequestForPersonality,
|
|
9
|
+
buildSystemPrompt,
|
|
10
|
+
GIT_MEMORY_ENABLED_TAG,
|
|
11
|
+
LETTA_CODE_ORIGIN_TAG,
|
|
12
|
+
} from "@letta-ai/letta-code/agent-presets";
|
|
13
|
+
import { releaseAppServerManagementConnections } from "./app-server-management.js";
|
|
14
|
+
import {
|
|
15
|
+
buildCanUseToolContext,
|
|
16
|
+
isHeadlessAutoAllowTool,
|
|
17
|
+
requiresRuntimeUserInput,
|
|
18
|
+
} from "./interactiveToolPolicy.js";
|
|
19
|
+
import { applyUniqueRequestIds } from "./request-ids.js";
|
|
20
|
+
import {
|
|
21
|
+
RemoteClientSessionCore,
|
|
22
|
+
ensureSuccess,
|
|
23
|
+
isUnrestrictedPermissionMode,
|
|
24
|
+
mapPermissionMode,
|
|
25
|
+
normalizeSendMessage,
|
|
26
|
+
type ProtocolMessage,
|
|
27
|
+
type RemoteClientRuntimeController,
|
|
28
|
+
type RuntimeSendTurnOptions,
|
|
29
|
+
type RuntimeScope,
|
|
30
|
+
type RuntimeSessionInit,
|
|
31
|
+
type RuntimeSessionMode,
|
|
32
|
+
} from "./remote-client-session-core.js";
|
|
33
|
+
import type {
|
|
34
|
+
AnyAgentTool,
|
|
35
|
+
CanUseToolContext,
|
|
36
|
+
CanUseToolResponse,
|
|
37
|
+
CreateAgentOptions,
|
|
38
|
+
LettaCodeRemoteClientOptions,
|
|
39
|
+
LettaCodeClientSessionOptions,
|
|
40
|
+
ListModelsResult,
|
|
41
|
+
ListMessagesOptions,
|
|
42
|
+
ListMessagesResult,
|
|
43
|
+
MessageContentItem,
|
|
44
|
+
RecoverPendingApprovalsOptions,
|
|
45
|
+
RecoverPendingApprovalsResult,
|
|
46
|
+
SendMessage,
|
|
47
|
+
UpdateModelResult,
|
|
48
|
+
} from "./types.js";
|
|
49
|
+
|
|
50
|
+
type RuntimeStartResponse = ProtocolMessage & {
|
|
51
|
+
type: "runtime_start_response";
|
|
52
|
+
success: boolean;
|
|
53
|
+
runtime: RuntimeScope | null;
|
|
54
|
+
agent: (Record<string, unknown> & {
|
|
55
|
+
id?: string;
|
|
56
|
+
model?: string | null;
|
|
57
|
+
model_settings?: Record<string, unknown> | null;
|
|
58
|
+
}) | null;
|
|
59
|
+
conversation: (Record<string, unknown> & { id?: string; agent_id?: string }) | null;
|
|
60
|
+
error?: string;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
type ConversationRetrieveResponse = ProtocolMessage & {
|
|
64
|
+
type: "conversation_retrieve_response";
|
|
65
|
+
success: boolean;
|
|
66
|
+
conversation: (Record<string, unknown> & { id?: string; agent_id?: string }) | null;
|
|
67
|
+
error?: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type ConversationMessagesListResponse = ProtocolMessage & {
|
|
71
|
+
type: "conversation_messages_list_response";
|
|
72
|
+
success: boolean;
|
|
73
|
+
messages: unknown[];
|
|
74
|
+
next_before?: string | null;
|
|
75
|
+
nextBefore?: string | null;
|
|
76
|
+
has_more?: boolean;
|
|
77
|
+
hasMore?: boolean;
|
|
78
|
+
error?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type ListModelsResponse = ProtocolMessage & {
|
|
82
|
+
type: "list_models_response";
|
|
83
|
+
success: boolean;
|
|
84
|
+
entries?: unknown;
|
|
85
|
+
available_handles?: unknown;
|
|
86
|
+
byok_provider_aliases?: unknown;
|
|
87
|
+
error?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type UpdateModelResponse = ProtocolMessage & {
|
|
91
|
+
type: "update_model_response";
|
|
92
|
+
success: boolean;
|
|
93
|
+
applied_to?: unknown;
|
|
94
|
+
model_id?: unknown;
|
|
95
|
+
model_handle?: unknown;
|
|
96
|
+
model_settings?: unknown;
|
|
97
|
+
error?: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type UpdateModelPayload = {
|
|
101
|
+
model_id?: string;
|
|
102
|
+
model_handle?: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
type RuntimeStartCommand = Parameters<AppServerClient["runtimeStart"]>[0];
|
|
106
|
+
|
|
107
|
+
export function agentToolNames(
|
|
108
|
+
agent: Record<string, unknown> | null | undefined,
|
|
109
|
+
): string[] | undefined {
|
|
110
|
+
const tools = agent?.tools;
|
|
111
|
+
if (!Array.isArray(tools)) return undefined;
|
|
112
|
+
return tools.flatMap((tool) => {
|
|
113
|
+
if (typeof tool === "string" && tool.length > 0) return [tool];
|
|
114
|
+
if (!tool || typeof tool !== "object") return [];
|
|
115
|
+
const name = (tool as { name?: unknown }).name;
|
|
116
|
+
return typeof name === "string" && name.length > 0 ? [name] : [];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type AppServerSessionOptions = Partial<LettaCodeRemoteClientOptions> & {
|
|
121
|
+
/** Base websocket URL. */
|
|
122
|
+
url?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Internal lazy connection hook. The Node entry point uses this to start a
|
|
125
|
+
* local app-server without pulling process-management code into `/client`.
|
|
126
|
+
*/
|
|
127
|
+
connect?: (
|
|
128
|
+
sessionEnv?: Record<string, string>,
|
|
129
|
+
) => Promise<{ url: string; close(): void }>;
|
|
130
|
+
/**
|
|
131
|
+
* Internal handoff hook used to release a management connection owned by
|
|
132
|
+
* the same SDK client before this session opens its control socket.
|
|
133
|
+
*/
|
|
134
|
+
beforeConnect?: () => Promise<void>;
|
|
135
|
+
/** Whether SDK create-agent payloads should add the origin tag automatically. */
|
|
136
|
+
includeSdkOriginTag?: boolean;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type AppServerSessionMode = RuntimeSessionMode;
|
|
140
|
+
|
|
141
|
+
function isPresetSystemPrompt(value: string): boolean {
|
|
142
|
+
return [
|
|
143
|
+
"default",
|
|
144
|
+
"letta-claude",
|
|
145
|
+
"letta-codex",
|
|
146
|
+
"letta-gemini",
|
|
147
|
+
"claude",
|
|
148
|
+
"codex",
|
|
149
|
+
"gemini",
|
|
150
|
+
].includes(value);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function assertRemoteCreateAgentOptionsSupported(options: CreateAgentOptions): void {
|
|
154
|
+
if (options.allowedTools !== undefined || options.disallowedTools !== undefined) {
|
|
155
|
+
throw new Error("App-server createAgent() does not yet support allowedTools/disallowedTools.");
|
|
156
|
+
}
|
|
157
|
+
if (options.canUseTool !== undefined) {
|
|
158
|
+
throw new Error("App-server createAgent() does not yet support canUseTool callbacks.");
|
|
159
|
+
}
|
|
160
|
+
if (options.systemInfoReminder !== undefined) {
|
|
161
|
+
throw new Error("App-server createAgent() does not yet support systemInfoReminder overrides.");
|
|
162
|
+
}
|
|
163
|
+
if (options.dreaming?.behavior !== undefined) {
|
|
164
|
+
throw new Error("App-server createAgent() does not yet support dreaming.behavior overrides.");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function assertRemoteSessionOptionsSupported(
|
|
169
|
+
action: string,
|
|
170
|
+
options: LettaCodeClientSessionOptions,
|
|
171
|
+
): void {
|
|
172
|
+
if (options.systemPrompt !== undefined) {
|
|
173
|
+
throw new Error(`App-server ${action}() does not yet support systemPrompt overrides for existing agents.`);
|
|
174
|
+
}
|
|
175
|
+
if (options.disallowedTools !== undefined) {
|
|
176
|
+
throw new Error(`App-server ${action}() does not yet support disallowedTools.`);
|
|
177
|
+
}
|
|
178
|
+
if (options.systemInfoReminder !== undefined) {
|
|
179
|
+
throw new Error(`App-server ${action}() does not yet support systemInfoReminder overrides.`);
|
|
180
|
+
}
|
|
181
|
+
if (options.dreaming?.behavior !== undefined) {
|
|
182
|
+
throw new Error(`App-server ${action}() does not yet support dreaming.behavior overrides.`);
|
|
183
|
+
}
|
|
184
|
+
if ((options as { memfsStartup?: unknown }).memfsStartup !== undefined) {
|
|
185
|
+
throw new Error(`App-server ${action}() does not support memfsStartup.`);
|
|
186
|
+
}
|
|
187
|
+
if (options.includePartialMessages !== undefined) {
|
|
188
|
+
throw new Error(`App-server ${action}() streams app-server deltas directly and does not support includePartialMessages.`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeMemoryBlock(block: Record<string, unknown>): Record<string, unknown> {
|
|
193
|
+
const normalized = { ...block };
|
|
194
|
+
if (normalized.value === undefined && typeof normalized.content === "string") {
|
|
195
|
+
normalized.value = normalized.content;
|
|
196
|
+
}
|
|
197
|
+
return normalized;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function upsertMemoryBlock(
|
|
201
|
+
blocks: Array<Record<string, unknown>>,
|
|
202
|
+
block: Record<string, unknown>,
|
|
203
|
+
): void {
|
|
204
|
+
const label = block.label;
|
|
205
|
+
if (typeof label === "string") {
|
|
206
|
+
const existingIndex = blocks.findIndex((candidate) => candidate.label === label);
|
|
207
|
+
if (existingIndex >= 0) {
|
|
208
|
+
blocks[existingIndex] = block;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
blocks.push(block);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function createAgentBody(
|
|
216
|
+
options: CreateAgentOptions,
|
|
217
|
+
settings: { includeSdkOriginTag?: boolean } = {},
|
|
218
|
+
): Promise<Record<string, unknown>> {
|
|
219
|
+
assertRemoteCreateAgentOptionsSupported(options);
|
|
220
|
+
|
|
221
|
+
const includeOriginTag = settings.includeSdkOriginTag ?? true;
|
|
222
|
+
const body: Record<string, unknown> = {
|
|
223
|
+
...(await buildCreateAgentRequestForPersonality({
|
|
224
|
+
personalityId: options.personality ?? "memo",
|
|
225
|
+
...(options.name !== undefined ? { name: options.name } : {}),
|
|
226
|
+
...(options.description !== undefined
|
|
227
|
+
? { description: options.description }
|
|
228
|
+
: {}),
|
|
229
|
+
...(options.model !== undefined ? { model: options.model } : {}),
|
|
230
|
+
...(options.tags !== undefined ? { extraTags: options.tags } : {}),
|
|
231
|
+
})),
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (Array.isArray(body.tags)) {
|
|
235
|
+
body.tags = body.tags.filter(
|
|
236
|
+
(tag) =>
|
|
237
|
+
(includeOriginTag || tag !== LETTA_CODE_ORIGIN_TAG) &&
|
|
238
|
+
(options.memfs !== false || tag !== GIT_MEMORY_ENABLED_TAG),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (options.embedding !== undefined) body.embedding = options.embedding;
|
|
243
|
+
if (options.hidden !== undefined) body.hidden = options.hidden;
|
|
244
|
+
// When baseTools is omitted, the harness applies its created-agent
|
|
245
|
+
// defaults (web_search, fetch_webpage). An explicit list is pinned exactly:
|
|
246
|
+
// `tools` alone does not suppress the Letta agent type's defaults, so both
|
|
247
|
+
// default tool attachment and its default tool rules are disabled here.
|
|
248
|
+
if (options.baseTools === undefined) {
|
|
249
|
+
delete body.tools;
|
|
250
|
+
delete body.include_base_tools;
|
|
251
|
+
delete body.include_base_tool_rules;
|
|
252
|
+
} else {
|
|
253
|
+
body.tools = options.baseTools;
|
|
254
|
+
body.include_base_tools = false;
|
|
255
|
+
body.include_base_tool_rules = false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (options.systemPrompt === undefined) {
|
|
259
|
+
if (options.memfs === false) {
|
|
260
|
+
body.system = buildSystemPrompt("default", "standard");
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
if (typeof options.systemPrompt === "string") {
|
|
264
|
+
if (isPresetSystemPrompt(options.systemPrompt)) {
|
|
265
|
+
throw new Error("createAgent() does not yet support system prompt presets for this backend.");
|
|
266
|
+
}
|
|
267
|
+
body.system = options.systemPrompt;
|
|
268
|
+
} else {
|
|
269
|
+
throw new Error("createAgent() does not yet support system prompt preset objects for this backend.");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const memoryBlocks = Array.isArray(body.memory_blocks)
|
|
274
|
+
? body.memory_blocks
|
|
275
|
+
.filter(
|
|
276
|
+
(block): block is Record<string, unknown> =>
|
|
277
|
+
block !== null && typeof block === "object",
|
|
278
|
+
)
|
|
279
|
+
.map((block) => ({ ...block }))
|
|
280
|
+
: [];
|
|
281
|
+
const blockIds: string[] = [];
|
|
282
|
+
for (const item of options.memory ?? []) {
|
|
283
|
+
if (typeof item === "string") {
|
|
284
|
+
throw new Error("App-server createAgent() does not yet support memory preset names.");
|
|
285
|
+
}
|
|
286
|
+
if ("blockId" in item) {
|
|
287
|
+
blockIds.push(item.blockId);
|
|
288
|
+
} else {
|
|
289
|
+
upsertMemoryBlock(
|
|
290
|
+
memoryBlocks,
|
|
291
|
+
normalizeMemoryBlock(item as unknown as Record<string, unknown>),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (options.persona !== undefined) {
|
|
296
|
+
upsertMemoryBlock(memoryBlocks, {
|
|
297
|
+
label: "persona",
|
|
298
|
+
value: options.persona,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (options.human !== undefined) {
|
|
302
|
+
upsertMemoryBlock(memoryBlocks, { label: "human", value: options.human });
|
|
303
|
+
}
|
|
304
|
+
body.memory_blocks = memoryBlocks;
|
|
305
|
+
if (blockIds.length > 0) body.block_ids = blockIds;
|
|
306
|
+
|
|
307
|
+
return body;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function externalToolGroups(tools: AnyAgentTool[] | undefined): Array<Record<string, unknown>> | undefined {
|
|
311
|
+
if (!tools || tools.length === 0) return undefined;
|
|
312
|
+
return [
|
|
313
|
+
{
|
|
314
|
+
tools: tools.map((tool) => ({
|
|
315
|
+
name: tool.name,
|
|
316
|
+
label: tool.label,
|
|
317
|
+
description: tool.description,
|
|
318
|
+
parameters: tool.parameters as Record<string, unknown>,
|
|
319
|
+
})),
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function createExternalToolCallHandler(
|
|
325
|
+
externalTools: Map<string, AnyAgentTool>,
|
|
326
|
+
): AppServerExternalToolCallHandler {
|
|
327
|
+
return async (request) => {
|
|
328
|
+
const tool = externalTools.get(request.tool_name);
|
|
329
|
+
if (!tool) {
|
|
330
|
+
throw new Error(`Unknown external tool: ${request.tool_name}`);
|
|
331
|
+
}
|
|
332
|
+
const result = await tool.execute(request.tool_call_id, request.input);
|
|
333
|
+
return {
|
|
334
|
+
content: result.content.map((part) => ({
|
|
335
|
+
type: part.type,
|
|
336
|
+
...(part.text !== undefined ? { text: part.text } : {}),
|
|
337
|
+
...(part.data !== undefined ? { data: part.data } : {}),
|
|
338
|
+
...(part.mimeType !== undefined ? { mimeType: part.mimeType } : {}),
|
|
339
|
+
})),
|
|
340
|
+
};
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
type AppServerApprovalOptions = LettaCodeClientSessionOptions | CreateAgentOptions;
|
|
345
|
+
|
|
346
|
+
type AppServerApprovalResponseDecision =
|
|
347
|
+
| {
|
|
348
|
+
behavior: "allow";
|
|
349
|
+
message?: string;
|
|
350
|
+
updated_input?: Record<string, unknown> | null;
|
|
351
|
+
selected_permission_suggestion_ids?: string[];
|
|
352
|
+
}
|
|
353
|
+
| {
|
|
354
|
+
behavior: "deny";
|
|
355
|
+
message: string;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
export async function resolveAppServerToolApproval(
|
|
359
|
+
options: AppServerApprovalOptions,
|
|
360
|
+
toolName: string,
|
|
361
|
+
toolInput: Record<string, unknown>,
|
|
362
|
+
context?: CanUseToolContext,
|
|
363
|
+
): Promise<CanUseToolResponse> {
|
|
364
|
+
const hasCallback = typeof options.canUseTool === "function";
|
|
365
|
+
const toolNeedsRuntimeUserInput = requiresRuntimeUserInput(toolName);
|
|
366
|
+
|
|
367
|
+
if (toolNeedsRuntimeUserInput && !hasCallback) {
|
|
368
|
+
return {
|
|
369
|
+
behavior: "deny",
|
|
370
|
+
message: "No canUseTool callback registered",
|
|
371
|
+
interrupt: false,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (isUnrestrictedPermissionMode(options.permissionMode) && !toolNeedsRuntimeUserInput) {
|
|
376
|
+
return { behavior: "allow", updatedInput: null, updatedPermissions: [] };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (hasCallback) {
|
|
380
|
+
try {
|
|
381
|
+
const result = await options.canUseTool!(toolName, toolInput, context);
|
|
382
|
+
if (result.behavior === "allow") {
|
|
383
|
+
return {
|
|
384
|
+
behavior: "allow",
|
|
385
|
+
message: result.message,
|
|
386
|
+
updatedInput: result.updatedInput ?? null,
|
|
387
|
+
updatedPermissions: result.updatedPermissions ?? [],
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
behavior: "deny",
|
|
392
|
+
message: result.message ?? "Denied by canUseTool callback",
|
|
393
|
+
interrupt: result.interrupt ?? false,
|
|
394
|
+
};
|
|
395
|
+
} catch (error) {
|
|
396
|
+
return {
|
|
397
|
+
behavior: "deny",
|
|
398
|
+
message: error instanceof Error ? error.message : "Callback error",
|
|
399
|
+
interrupt: false,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (isHeadlessAutoAllowTool(toolName)) {
|
|
405
|
+
return { behavior: "allow", updatedInput: null, updatedPermissions: [] };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
behavior: "deny",
|
|
410
|
+
message: "No canUseTool callback registered",
|
|
411
|
+
interrupt: false,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function permissionSuggestionId(value: unknown): string | null {
|
|
416
|
+
if (typeof value === "string") return value;
|
|
417
|
+
if (!value || typeof value !== "object") return null;
|
|
418
|
+
const record = value as Record<string, unknown>;
|
|
419
|
+
if (typeof record.id === "string") return record.id;
|
|
420
|
+
if (typeof record.suggestion_id === "string") return record.suggestion_id;
|
|
421
|
+
if (typeof record.permission_suggestion_id === "string") return record.permission_suggestion_id;
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function toAppServerApprovalDecision(
|
|
426
|
+
decision: CanUseToolResponse,
|
|
427
|
+
): AppServerApprovalResponseDecision {
|
|
428
|
+
if (decision.behavior === "deny") {
|
|
429
|
+
return {
|
|
430
|
+
behavior: "deny",
|
|
431
|
+
message: decision.message,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const selectedPermissionSuggestionIds = (decision.updatedPermissions ?? [])
|
|
436
|
+
.map(permissionSuggestionId)
|
|
437
|
+
.filter((id): id is string => id !== null);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
behavior: "allow",
|
|
441
|
+
...(decision.message !== undefined ? { message: decision.message } : {}),
|
|
442
|
+
updated_input: decision.updatedInput ?? null,
|
|
443
|
+
selected_permission_suggestion_ids: selectedPermissionSuggestionIds,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function stringRecord(value: unknown): Record<string, string> | undefined {
|
|
448
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
449
|
+
const record: Record<string, string> = {};
|
|
450
|
+
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
|
451
|
+
if (typeof entry === "string") record[key] = entry;
|
|
452
|
+
}
|
|
453
|
+
return record;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function objectRecord(value: unknown): Record<string, unknown> | undefined {
|
|
457
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
458
|
+
return { ...(value as Record<string, unknown>) };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function normalizeListModelsResponse(response: ListModelsResponse): ListModelsResult {
|
|
462
|
+
if (!response.success) {
|
|
463
|
+
throw new Error(response.error ?? "listModels failed");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const entries = Array.isArray(response.entries)
|
|
467
|
+
? response.entries.flatMap((entry) => {
|
|
468
|
+
if (!entry || typeof entry !== "object") return [];
|
|
469
|
+
const record = entry as Record<string, unknown>;
|
|
470
|
+
if (
|
|
471
|
+
typeof record.id !== "string" ||
|
|
472
|
+
typeof record.handle !== "string" ||
|
|
473
|
+
typeof record.label !== "string" ||
|
|
474
|
+
typeof record.description !== "string"
|
|
475
|
+
) {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
const updateArgs = objectRecord(record.updateArgs);
|
|
479
|
+
return [
|
|
480
|
+
{
|
|
481
|
+
id: record.id,
|
|
482
|
+
handle: record.handle,
|
|
483
|
+
label: record.label,
|
|
484
|
+
description: record.description,
|
|
485
|
+
...(typeof record.isDefault === "boolean" ? { isDefault: record.isDefault } : {}),
|
|
486
|
+
...(typeof record.isFeatured === "boolean" ? { isFeatured: record.isFeatured } : {}),
|
|
487
|
+
...(typeof record.free === "boolean" ? { free: record.free } : {}),
|
|
488
|
+
...(updateArgs ? { updateArgs } : {}),
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
})
|
|
492
|
+
: [];
|
|
493
|
+
|
|
494
|
+
const result: ListModelsResult = { entries };
|
|
495
|
+
if (response.available_handles === null) {
|
|
496
|
+
result.availableHandles = null;
|
|
497
|
+
} else if (Array.isArray(response.available_handles)) {
|
|
498
|
+
result.availableHandles = response.available_handles.filter(
|
|
499
|
+
(handle): handle is string => typeof handle === "string",
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
const aliases = stringRecord(response.byok_provider_aliases);
|
|
503
|
+
if (aliases) result.byokProviderAliases = aliases;
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function normalizeUpdateModelResponse(response: UpdateModelResponse): UpdateModelResult {
|
|
508
|
+
if (!response.success) {
|
|
509
|
+
throw new Error(response.error ?? "Failed to update model");
|
|
510
|
+
}
|
|
511
|
+
const result: UpdateModelResult = {};
|
|
512
|
+
if (response.applied_to === "agent" || response.applied_to === "conversation") {
|
|
513
|
+
result.appliedTo = response.applied_to;
|
|
514
|
+
}
|
|
515
|
+
if (typeof response.model_id === "string") result.modelId = response.model_id;
|
|
516
|
+
if (typeof response.model_handle === "string") result.modelHandle = response.model_handle;
|
|
517
|
+
if (response.model_settings === null) {
|
|
518
|
+
result.modelSettings = null;
|
|
519
|
+
} else if (response.model_settings && typeof response.model_settings === "object") {
|
|
520
|
+
result.modelSettings = { ...(response.model_settings as Record<string, unknown>) };
|
|
521
|
+
}
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function runtimeScopeFromMessage(message: ProtocolMessage): RuntimeScope | null {
|
|
526
|
+
if (message.runtime) return message.runtime;
|
|
527
|
+
const agentId = typeof message.agent_id === "string" ? message.agent_id : null;
|
|
528
|
+
const conversationId = typeof message.conversation_id === "string" ? message.conversation_id : null;
|
|
529
|
+
if (agentId && conversationId) {
|
|
530
|
+
return { agent_id: agentId, conversation_id: conversationId };
|
|
531
|
+
}
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function registerAppServerControlRequestHandler(config: {
|
|
536
|
+
client: AppServerClient;
|
|
537
|
+
getRuntime: () => RuntimeScope | null;
|
|
538
|
+
getOptions: () => AppServerApprovalOptions;
|
|
539
|
+
}): () => void {
|
|
540
|
+
return config.client.onMessage((rawMessage, channel) => {
|
|
541
|
+
const message = rawMessage as unknown as ProtocolMessage;
|
|
542
|
+
if (channel !== "control" || message.type !== "control_request") return;
|
|
543
|
+
void respondToAppServerControlRequest(config, message).catch(() => {
|
|
544
|
+
// Permission callback failures are converted into deny responses by resolveAppServerToolApproval().
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function respondToAppServerControlRequest(
|
|
550
|
+
config: {
|
|
551
|
+
client: AppServerClient;
|
|
552
|
+
getRuntime: () => RuntimeScope | null;
|
|
553
|
+
getOptions: () => AppServerApprovalOptions;
|
|
554
|
+
},
|
|
555
|
+
message: ProtocolMessage,
|
|
556
|
+
): Promise<void> {
|
|
557
|
+
const runtime = runtimeScopeFromMessage(message) ?? config.getRuntime();
|
|
558
|
+
if (!runtime) return;
|
|
559
|
+
|
|
560
|
+
const requestId = typeof message.request_id === "string" ? message.request_id : undefined;
|
|
561
|
+
const request = message.request;
|
|
562
|
+
if (!requestId || !request || typeof request !== "object") return;
|
|
563
|
+
const requestRecord = request as Record<string, unknown>;
|
|
564
|
+
if (requestRecord.subtype !== "can_use_tool") return;
|
|
565
|
+
|
|
566
|
+
const toolName = typeof requestRecord.tool_name === "string" ? requestRecord.tool_name : "unknown";
|
|
567
|
+
const toolInput =
|
|
568
|
+
requestRecord.input && typeof requestRecord.input === "object" && !Array.isArray(requestRecord.input)
|
|
569
|
+
? (requestRecord.input as Record<string, unknown>)
|
|
570
|
+
: {};
|
|
571
|
+
const decision = await resolveAppServerToolApproval(
|
|
572
|
+
config.getOptions(),
|
|
573
|
+
toolName,
|
|
574
|
+
toolInput,
|
|
575
|
+
buildCanUseToolContext(requestRecord, requestId),
|
|
576
|
+
);
|
|
577
|
+
config.client.input({
|
|
578
|
+
runtime,
|
|
579
|
+
payload: {
|
|
580
|
+
kind: "approval_response",
|
|
581
|
+
request_id: requestId,
|
|
582
|
+
decision: toAppServerApprovalDecision(decision),
|
|
583
|
+
},
|
|
584
|
+
} as Parameters<AppServerClient["input"]>[0]);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export class AppServerRuntimeController implements RemoteClientRuntimeController {
|
|
588
|
+
constructor(
|
|
589
|
+
private readonly client: AppServerClient,
|
|
590
|
+
private readonly options: AppServerSessionOptions,
|
|
591
|
+
private readonly clientToolAllowlist?: readonly string[],
|
|
592
|
+
) {}
|
|
593
|
+
|
|
594
|
+
onMessage(handler: (message: ProtocolMessage, channel?: string) => void): () => void {
|
|
595
|
+
return this.client.onMessage((message, channel) => {
|
|
596
|
+
handler(message as unknown as ProtocolMessage, channel);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
send(command: Record<string, unknown>): void {
|
|
601
|
+
this.client.send(command as unknown as Parameters<AppServerClient["send"]>[0]);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
sendTurnMessage(
|
|
605
|
+
runtime: RuntimeScope,
|
|
606
|
+
message: SendMessage,
|
|
607
|
+
options: RuntimeSendTurnOptions,
|
|
608
|
+
): void {
|
|
609
|
+
const payload: Record<string, unknown> = {
|
|
610
|
+
kind: "create_message",
|
|
611
|
+
messages: [
|
|
612
|
+
{
|
|
613
|
+
role: "user",
|
|
614
|
+
content: normalizeSendMessage(message),
|
|
615
|
+
client_message_id: options.clientMessageId,
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
};
|
|
619
|
+
if (this.clientToolAllowlist !== undefined) {
|
|
620
|
+
payload.client_tool_allowlist = [...new Set(this.clientToolAllowlist)];
|
|
621
|
+
}
|
|
622
|
+
// SDK sessions are headless: interactive user-input tools are always
|
|
623
|
+
// excluded from the toolset (harnesses without support ignore the flag
|
|
624
|
+
// and the SDK's runtime denial still applies).
|
|
625
|
+
payload.exclude_interactive_tools = true;
|
|
626
|
+
this.client.input({
|
|
627
|
+
runtime,
|
|
628
|
+
payload,
|
|
629
|
+
} as Parameters<AppServerClient["input"]>[0]);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
async abort(runtime: RuntimeScope): Promise<void> {
|
|
633
|
+
await this.client.abort({ runtime } as Parameters<AppServerClient["abort"]>[0]);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
request(
|
|
637
|
+
type: string,
|
|
638
|
+
body: Record<string, unknown>,
|
|
639
|
+
options: { timeoutMs?: number; predicate?: (message: ProtocolMessage) => boolean } = {},
|
|
640
|
+
): Promise<ProtocolMessage> {
|
|
641
|
+
const request = this.client.request.bind(this.client) as unknown as (
|
|
642
|
+
commandType: string,
|
|
643
|
+
commandBody: Record<string, unknown>,
|
|
644
|
+
requestOptions?: {
|
|
645
|
+
timeoutMs?: number;
|
|
646
|
+
predicate?: (message: ProtocolMessage) => boolean;
|
|
647
|
+
},
|
|
648
|
+
) => Promise<ProtocolMessage>;
|
|
649
|
+
return request(type, body, options);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async listModels(): Promise<ListModelsResult> {
|
|
653
|
+
const response = (await this.request(
|
|
654
|
+
"list_models",
|
|
655
|
+
{},
|
|
656
|
+
{ predicate: (message) => message.type === "list_models_response" },
|
|
657
|
+
)) as ListModelsResponse;
|
|
658
|
+
return normalizeListModelsResponse(response);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async updateModel(
|
|
662
|
+
runtime: RuntimeScope,
|
|
663
|
+
payload: UpdateModelPayload,
|
|
664
|
+
): Promise<UpdateModelResult> {
|
|
665
|
+
const response = (await this.request(
|
|
666
|
+
"update_model",
|
|
667
|
+
{
|
|
668
|
+
runtime,
|
|
669
|
+
payload,
|
|
670
|
+
},
|
|
671
|
+
{ predicate: (message) => message.type === "update_model_response" },
|
|
672
|
+
)) as UpdateModelResponse;
|
|
673
|
+
return normalizeUpdateModelResponse(response);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
async recoverPendingApprovals(
|
|
677
|
+
runtime: RuntimeScope,
|
|
678
|
+
options: RecoverPendingApprovalsOptions = {},
|
|
679
|
+
): Promise<RecoverPendingApprovalsResult> {
|
|
680
|
+
const response = await this.client.sync(
|
|
681
|
+
{
|
|
682
|
+
runtime,
|
|
683
|
+
recover_approvals: true,
|
|
684
|
+
force_device_status: true,
|
|
685
|
+
},
|
|
686
|
+
options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {},
|
|
687
|
+
);
|
|
688
|
+
|
|
689
|
+
if (!response.success) {
|
|
690
|
+
return {
|
|
691
|
+
recovered: false,
|
|
692
|
+
unsupported: false,
|
|
693
|
+
detail: response.error ?? "Failed to recover pending approvals",
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return { recovered: true, unsupported: false };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async listMessages(
|
|
701
|
+
conversationId: string,
|
|
702
|
+
options: ListMessagesOptions = {},
|
|
703
|
+
): Promise<ListMessagesResult> {
|
|
704
|
+
const query: Record<string, unknown> = {};
|
|
705
|
+
if (options.before !== undefined) query.before = options.before;
|
|
706
|
+
if (options.after !== undefined) query.after = options.after;
|
|
707
|
+
if (options.order !== undefined) query.order = options.order;
|
|
708
|
+
if (options.limit !== undefined) query.limit = options.limit;
|
|
709
|
+
|
|
710
|
+
const response = (await this.request(
|
|
711
|
+
"conversation_messages_list",
|
|
712
|
+
{
|
|
713
|
+
conversation_id: conversationId,
|
|
714
|
+
...(Object.keys(query).length > 0 ? { query } : {}),
|
|
715
|
+
},
|
|
716
|
+
{ predicate: (message) => message.type === "conversation_messages_list_response" },
|
|
717
|
+
)) as ConversationMessagesListResponse;
|
|
718
|
+
|
|
719
|
+
if (!response.success) {
|
|
720
|
+
throw new Error(response.error ?? "listMessages failed");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const result: ListMessagesResult = {
|
|
724
|
+
messages: response.messages ?? [],
|
|
725
|
+
};
|
|
726
|
+
const nextBefore =
|
|
727
|
+
typeof response.nextBefore === "string" || response.nextBefore === null
|
|
728
|
+
? response.nextBefore
|
|
729
|
+
: typeof response.next_before === "string" || response.next_before === null
|
|
730
|
+
? response.next_before
|
|
731
|
+
: undefined;
|
|
732
|
+
if (nextBefore !== undefined) result.nextBefore = nextBefore;
|
|
733
|
+
const hasMore =
|
|
734
|
+
typeof response.hasMore === "boolean"
|
|
735
|
+
? response.hasMore
|
|
736
|
+
: typeof response.has_more === "boolean"
|
|
737
|
+
? response.has_more
|
|
738
|
+
: undefined;
|
|
739
|
+
if (hasMore !== undefined) result.hasMore = hasMore;
|
|
740
|
+
return result;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
close(): void {
|
|
744
|
+
this.client.close();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
export class AppServerSession extends RemoteClientSessionCore {
|
|
749
|
+
private ownedConnection: { close(): void } | null = null;
|
|
750
|
+
private externalTools = new Map<string, AnyAgentTool>();
|
|
751
|
+
private removeExternalToolHandler: (() => void) | null = null;
|
|
752
|
+
private removeControlRequestHandler: (() => void) | null = null;
|
|
753
|
+
|
|
754
|
+
constructor(
|
|
755
|
+
private readonly remoteOptions: AppServerSessionOptions,
|
|
756
|
+
mode: AppServerSessionMode,
|
|
757
|
+
) {
|
|
758
|
+
super(mode, {
|
|
759
|
+
label: "app-server",
|
|
760
|
+
requestTimeoutMs: remoteOptions.requestTimeoutMs,
|
|
761
|
+
});
|
|
762
|
+
const tools = mode.options.tools;
|
|
763
|
+
for (const tool of tools ?? []) {
|
|
764
|
+
this.externalTools.set(tool.name, tool);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
protected override shouldEnableMemfs(options: LettaCodeClientSessionOptions | CreateAgentOptions): boolean {
|
|
769
|
+
if (this.mode.kind !== "create-agent") return false;
|
|
770
|
+
return (options as CreateAgentOptions).memfs !== false;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
protected override async initializeRuntimeController(): Promise<RuntimeSessionInit> {
|
|
774
|
+
await this.remoteOptions.beforeConnect?.();
|
|
775
|
+
const url = await this.resolveAppServerUrl();
|
|
776
|
+
await releaseAppServerManagementConnections(url);
|
|
777
|
+
const client = applyUniqueRequestIds(createAppServerClient({
|
|
778
|
+
url,
|
|
779
|
+
...(this.remoteOptions.authToken !== undefined
|
|
780
|
+
? { authToken: this.remoteOptions.authToken }
|
|
781
|
+
: {}),
|
|
782
|
+
...(this.remoteOptions.WebSocket
|
|
783
|
+
? { WebSocket: this.remoteOptions.WebSocket as AppServerSocketConstructor }
|
|
784
|
+
: {}),
|
|
785
|
+
...(this.remoteOptions.requestTimeoutMs !== undefined
|
|
786
|
+
? { requestTimeoutMs: this.remoteOptions.requestTimeoutMs }
|
|
787
|
+
: {}),
|
|
788
|
+
}));
|
|
789
|
+
|
|
790
|
+
this.removeControlRequestHandler = registerAppServerControlRequestHandler({
|
|
791
|
+
client,
|
|
792
|
+
getRuntime: () => this.runtime,
|
|
793
|
+
getOptions: () => this.currentOptions(),
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
if (this.externalTools.size > 0) {
|
|
797
|
+
this.removeExternalToolHandler = client.onExternalToolCall(
|
|
798
|
+
createExternalToolCallHandler(this.externalTools),
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
await client.connect();
|
|
804
|
+
const response = await this.startRuntime(client);
|
|
805
|
+
if (!response.success || !response.runtime) {
|
|
806
|
+
throw new Error(response.error ?? "Failed to start app-server runtime");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const tools = agentToolNames(response.agent);
|
|
810
|
+
const skillSources = this.currentOptions().skillSources;
|
|
811
|
+
return {
|
|
812
|
+
controller: new AppServerRuntimeController(
|
|
813
|
+
client,
|
|
814
|
+
this.remoteOptions,
|
|
815
|
+
this.currentOptions().allowedTools,
|
|
816
|
+
),
|
|
817
|
+
runtime: response.runtime,
|
|
818
|
+
model: typeof response.agent?.model === "string" ? response.agent.model : "",
|
|
819
|
+
modelSettings: response.agent?.model_settings ?? null,
|
|
820
|
+
...(tools !== undefined ? { tools } : {}),
|
|
821
|
+
...(skillSources !== undefined ? { skillSources: [...skillSources] } : {}),
|
|
822
|
+
};
|
|
823
|
+
} catch (error) {
|
|
824
|
+
this.removeExternalToolHandler?.();
|
|
825
|
+
this.removeExternalToolHandler = null;
|
|
826
|
+
this.removeControlRequestHandler?.();
|
|
827
|
+
this.removeControlRequestHandler = null;
|
|
828
|
+
client.close();
|
|
829
|
+
throw error;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
protected override onCoreClose(): void {
|
|
834
|
+
this.removeExternalToolHandler?.();
|
|
835
|
+
this.removeExternalToolHandler = null;
|
|
836
|
+
this.removeControlRequestHandler?.();
|
|
837
|
+
this.removeControlRequestHandler = null;
|
|
838
|
+
this.ownedConnection?.close();
|
|
839
|
+
this.ownedConnection = null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
private async resolveAppServerUrl(): Promise<string> {
|
|
843
|
+
if (this.remoteOptions.url) {
|
|
844
|
+
return this.remoteOptions.url;
|
|
845
|
+
}
|
|
846
|
+
if (!this.remoteOptions.connect) {
|
|
847
|
+
throw new Error("App-server session requires a url.");
|
|
848
|
+
}
|
|
849
|
+
const sessionEnv = (
|
|
850
|
+
this.mode.options as { env?: Record<string, string> }
|
|
851
|
+
).env;
|
|
852
|
+
const connection = await this.remoteOptions.connect(sessionEnv);
|
|
853
|
+
this.ownedConnection = connection;
|
|
854
|
+
return connection.url;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
private async startRuntime(client: AppServerClient): Promise<RuntimeStartResponse> {
|
|
858
|
+
const command = await this.buildRuntimeStartCommand(client);
|
|
859
|
+
const response = (await client.runtimeStart(command)) as unknown as RuntimeStartResponse;
|
|
860
|
+
return response;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
private async buildRuntimeStartCommand(client: AppServerClient): Promise<RuntimeStartCommand> {
|
|
864
|
+
const options = this.mode.options;
|
|
865
|
+
const command: Record<string, unknown> = {
|
|
866
|
+
client_info: {
|
|
867
|
+
name: "@letta-ai/letta-agent-sdk",
|
|
868
|
+
title: "Letta Agent SDK",
|
|
869
|
+
},
|
|
870
|
+
recover_approvals: false,
|
|
871
|
+
force_device_status: true,
|
|
872
|
+
};
|
|
873
|
+
|
|
874
|
+
const mode = mapPermissionMode(options.permissionMode);
|
|
875
|
+
if (mode) command.mode = mode;
|
|
876
|
+
if (options.cwd !== undefined) command.cwd = options.cwd;
|
|
877
|
+
// Keep the distinction between omitted (use harness defaults) and []
|
|
878
|
+
// (disable bundled/global/agent/project skills). The app-server runtime is
|
|
879
|
+
// session-scoped, so this must be sent on creation and every resume.
|
|
880
|
+
if (options.skillSources !== undefined) {
|
|
881
|
+
command.skill_sources = [...new Set(options.skillSources)];
|
|
882
|
+
}
|
|
883
|
+
const groups = externalToolGroups(options.tools);
|
|
884
|
+
if (groups) command.external_tools = groups;
|
|
885
|
+
|
|
886
|
+
if (this.mode.kind === "create-agent") {
|
|
887
|
+
command.create_agent = {
|
|
888
|
+
body: await createAgentBody(this.mode.options, {
|
|
889
|
+
includeSdkOriginTag: this.remoteOptions.includeSdkOriginTag,
|
|
890
|
+
}),
|
|
891
|
+
// Hidden (worker-style) agents default to unpinned.
|
|
892
|
+
pin_global:
|
|
893
|
+
this.remoteOptions.pinGlobalAgent ??
|
|
894
|
+
(this.mode.options as CreateAgentOptions).hidden !== true,
|
|
895
|
+
// Signal memfs-less (worker-style) creation to the harness so it
|
|
896
|
+
// skips the memfs tag/settings/clone entirely; older harnesses
|
|
897
|
+
// ignore the field, and the client-side enable_memfs skip still
|
|
898
|
+
// applies either way.
|
|
899
|
+
...((this.mode.options as CreateAgentOptions).memfs === false
|
|
900
|
+
? { memfs: false }
|
|
901
|
+
: {}),
|
|
902
|
+
};
|
|
903
|
+
return command as RuntimeStartCommand;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (this.mode.agentId) {
|
|
907
|
+
command.agent_id = this.mode.agentId;
|
|
908
|
+
if (this.mode.newConversation) {
|
|
909
|
+
command.create_conversation = { body: {} };
|
|
910
|
+
} else if (this.mode.defaultConversation) {
|
|
911
|
+
command.conversation_id = "default";
|
|
912
|
+
}
|
|
913
|
+
return command as RuntimeStartCommand;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (this.mode.conversationId) {
|
|
917
|
+
const agentId = await this.resolveConversationAgentId(client, this.mode.conversationId);
|
|
918
|
+
command.agent_id = agentId;
|
|
919
|
+
command.conversation_id = this.mode.conversationId;
|
|
920
|
+
return command as RuntimeStartCommand;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
throw new Error(
|
|
924
|
+
"App-server createSession() requires an agent id. Call createAgent() first or pass an agent id.",
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private async resolveConversationAgentId(
|
|
929
|
+
client: AppServerClient,
|
|
930
|
+
conversationId: string,
|
|
931
|
+
): Promise<string> {
|
|
932
|
+
const request = client.request.bind(client) as unknown as (
|
|
933
|
+
commandType: string,
|
|
934
|
+
commandBody: Record<string, unknown>,
|
|
935
|
+
options?: { predicate?: (message: ProtocolMessage) => boolean },
|
|
936
|
+
) => Promise<ProtocolMessage>;
|
|
937
|
+
const response = (await request(
|
|
938
|
+
"conversation_retrieve",
|
|
939
|
+
{ conversation_id: conversationId },
|
|
940
|
+
{ predicate: (message) => message.type === "conversation_retrieve_response" },
|
|
941
|
+
)) as ConversationRetrieveResponse;
|
|
942
|
+
if (!response.success || !response.conversation?.agent_id) {
|
|
943
|
+
throw new Error(response.error ?? `Failed to retrieve conversation ${conversationId}`);
|
|
944
|
+
}
|
|
945
|
+
return response.conversation.agent_id;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
}
|