@letta-ai/letta-agent-sdk 0.8.73 → 0.8.75
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 +69 -0
- package/dist/app-server-session.d.ts +28 -7
- package/dist/app-server-session.d.ts.map +1 -1
- package/dist/client-base.d.ts +17 -0
- package/dist/client-base.d.ts.map +1 -1
- package/dist/client-entry.d.ts +1 -0
- package/dist/client-entry.d.ts.map +1 -1
- package/dist/client-entry.js +488 -100
- package/dist/client-entry.js.map +16 -13
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/cloud-session.d.ts.map +1 -1
- package/dist/cloud-status-transport.d.ts.map +1 -1
- package/dist/external-tools.d.ts +11 -0
- package/dist/external-tools.d.ts.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +517 -126
- package/dist/index.js.map +18 -15
- package/dist/remote-client-session-core.d.ts.map +1 -1
- package/dist/remote-session-protocol.d.ts +3 -1
- package/dist/remote-session-protocol.d.ts.map +1 -1
- package/dist/remote-turn-coordinator.d.ts +14 -0
- package/dist/remote-turn-coordinator.d.ts.map +1 -1
- package/dist/runtime-tool-policy.d.ts +57 -0
- package/dist/runtime-tool-policy.d.ts.map +1 -0
- package/dist/structured-output-session.d.ts.map +1 -1
- package/dist/tool-registration.d.ts +67 -0
- package/dist/tool-registration.d.ts.map +1 -0
- package/dist/types.d.ts +30 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/app-server-session.ts +104 -60
- package/src/client-base.ts +57 -14
- package/src/client-entry.ts +1 -0
- package/src/client.ts +6 -2
- package/src/cloud-session.ts +12 -3
- package/src/cloud-status-transport.ts +4 -0
- package/src/external-tools.ts +69 -0
- package/src/index.ts +3 -0
- package/src/remote-client-session-core.ts +4 -4
- package/src/remote-session-protocol.ts +3 -0
- package/src/remote-turn-coordinator.ts +80 -15
- package/src/runtime-tool-policy.ts +114 -0
- package/src/structured-output-session.ts +2 -1
- package/src/tool-registration.ts +311 -0
- package/src/types.ts +39 -1
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createAppServerClient,
|
|
3
3
|
type AppServerClient,
|
|
4
|
-
type AppServerExternalToolCallHandler,
|
|
5
4
|
type AppServerSocketConstructor,
|
|
6
5
|
} from "@letta-ai/letta-code/app-server-client";
|
|
7
6
|
import type {
|
|
@@ -25,6 +24,16 @@ import {
|
|
|
25
24
|
type McpToolBridge,
|
|
26
25
|
} from "./mcp-runtime.js";
|
|
27
26
|
import { applyUniqueRequestIds } from "./request-ids.js";
|
|
27
|
+
import { createExternalToolCallHandler, externalToolGroups } from "./external-tools.js";
|
|
28
|
+
import {
|
|
29
|
+
clientToolsetPayload,
|
|
30
|
+
mergeInheritedTools,
|
|
31
|
+
runtimeToolPolicy,
|
|
32
|
+
supportsRuntimeToolPolicy,
|
|
33
|
+
type InheritedToolSource,
|
|
34
|
+
type RuntimeToolPolicy,
|
|
35
|
+
type SessionToolSelection,
|
|
36
|
+
} from "./runtime-tool-policy.js";
|
|
28
37
|
import {
|
|
29
38
|
RemoteClientSessionCore,
|
|
30
39
|
ensureSuccess,
|
|
@@ -104,41 +113,6 @@ export type AppServerSessionOptions = Partial<LettaCodeRemoteClientOptions> & {
|
|
|
104
113
|
|
|
105
114
|
export type AppServerSessionMode = RuntimeSessionMode;
|
|
106
115
|
|
|
107
|
-
export function externalToolGroups(tools: AnyAgentTool[] | undefined): Array<Record<string, unknown>> | undefined {
|
|
108
|
-
if (!tools || tools.length === 0) return undefined;
|
|
109
|
-
return [
|
|
110
|
-
{
|
|
111
|
-
tools: tools.map((tool) => ({
|
|
112
|
-
name: tool.name,
|
|
113
|
-
label: tool.label,
|
|
114
|
-
description: tool.description,
|
|
115
|
-
parameters: tool.parameters as Record<string, unknown>,
|
|
116
|
-
})),
|
|
117
|
-
},
|
|
118
|
-
];
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function createExternalToolCallHandler(
|
|
122
|
-
externalTools: Map<string, AnyAgentTool>,
|
|
123
|
-
): AppServerExternalToolCallHandler {
|
|
124
|
-
return async (request) => {
|
|
125
|
-
const tool = externalTools.get(request.tool_name);
|
|
126
|
-
if (!tool) {
|
|
127
|
-
throw new Error(`Unknown external tool: ${request.tool_name}`);
|
|
128
|
-
}
|
|
129
|
-
const result = await tool.execute(request.tool_call_id, request.input);
|
|
130
|
-
return {
|
|
131
|
-
content: result.content.map((part) => ({
|
|
132
|
-
type: part.type,
|
|
133
|
-
...(part.text !== undefined ? { text: part.text } : {}),
|
|
134
|
-
...(part.data !== undefined ? { data: part.data } : {}),
|
|
135
|
-
...(part.mimeType !== undefined ? { mimeType: part.mimeType } : {}),
|
|
136
|
-
})),
|
|
137
|
-
...(result.isError === true ? { is_error: true } : {}),
|
|
138
|
-
};
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
116
|
type AppServerApprovalOptions = LettaCodeClientSessionOptions | CreateAgentOptions;
|
|
143
117
|
|
|
144
118
|
type AppServerApprovalResponseDecision =
|
|
@@ -393,10 +367,32 @@ export class AppServerRuntimeController implements RemoteClientRuntimeController
|
|
|
393
367
|
constructor(
|
|
394
368
|
private readonly client: AppServerClient,
|
|
395
369
|
private readonly options: AppServerSessionOptions,
|
|
396
|
-
private
|
|
397
|
-
private
|
|
370
|
+
private clientToolAllowlist?: readonly string[],
|
|
371
|
+
private clientToolset?: ClientToolsetConfig,
|
|
372
|
+
/** The listener applies runtime_start's tool_policy to every turn. */
|
|
373
|
+
private readonly toolPolicyApplied = false,
|
|
398
374
|
) {}
|
|
399
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Replace this connection's tools and policy for the runtime, as when a
|
|
378
|
+
* `registerTools()` registration the session inherits changes. Older
|
|
379
|
+
* listeners get the tools here and the selection with each message.
|
|
380
|
+
*/
|
|
381
|
+
async updateToolSelection(runtime: RuntimeScope, selection: SessionToolSelection): Promise<void> {
|
|
382
|
+
this.clientToolAllowlist = selection.allowedTools;
|
|
383
|
+
this.clientToolset = selection.toolset;
|
|
384
|
+
const response = await this.client.runtimeExternalToolsUpdate({
|
|
385
|
+
updates: [{
|
|
386
|
+
runtimes: [runtime],
|
|
387
|
+
external_tools: externalToolGroups(selection.tools) ?? [],
|
|
388
|
+
...(this.toolPolicyApplied
|
|
389
|
+
? { tool_policy: runtimeToolPolicy(selection.allowedTools, selection.toolset) }
|
|
390
|
+
: {}),
|
|
391
|
+
}],
|
|
392
|
+
} as unknown as Parameters<AppServerClient["runtimeExternalToolsUpdate"]>[0]);
|
|
393
|
+
if (!response.success) throw new Error(response.error ?? "Failed to update session tools");
|
|
394
|
+
}
|
|
395
|
+
|
|
400
396
|
onMessage(handler: (message: ProtocolMessage, channel?: string) => void): () => void {
|
|
401
397
|
return this.client.onMessage((message, channel) => {
|
|
402
398
|
handler(message as unknown as ProtocolMessage, channel);
|
|
@@ -425,18 +421,12 @@ export class AppServerRuntimeController implements RemoteClientRuntimeController
|
|
|
425
421
|
},
|
|
426
422
|
],
|
|
427
423
|
};
|
|
428
|
-
|
|
424
|
+
// Older listeners only honour tool selection sent with each message.
|
|
425
|
+
if (!this.toolPolicyApplied && this.clientToolAllowlist !== undefined) {
|
|
429
426
|
payload.client_tool_allowlist = [...new Set(this.clientToolAllowlist)];
|
|
430
427
|
}
|
|
431
|
-
if (this.clientToolset !== undefined) {
|
|
432
|
-
payload.client_toolset =
|
|
433
|
-
...(this.clientToolset.base !== undefined
|
|
434
|
-
? { base: this.clientToolset.base }
|
|
435
|
-
: {}),
|
|
436
|
-
...(this.clientToolset.include !== undefined
|
|
437
|
-
? { include: [...new Set(this.clientToolset.include)] }
|
|
438
|
-
: {}),
|
|
439
|
-
};
|
|
428
|
+
if (!this.toolPolicyApplied && this.clientToolset !== undefined) {
|
|
429
|
+
payload.client_toolset = clientToolsetPayload(this.clientToolset);
|
|
440
430
|
}
|
|
441
431
|
if (options.outputFormat !== undefined) {
|
|
442
432
|
payload.response_format = {
|
|
@@ -451,7 +441,7 @@ export class AppServerRuntimeController implements RemoteClientRuntimeController
|
|
|
451
441
|
// SDK sessions are headless: interactive user-input tools are always
|
|
452
442
|
// excluded from the toolset (harnesses without support ignore the flag
|
|
453
443
|
// and the SDK's runtime denial still applies).
|
|
454
|
-
payload.exclude_interactive_tools = true;
|
|
444
|
+
if (!this.toolPolicyApplied) payload.exclude_interactive_tools = true;
|
|
455
445
|
this.client.input({
|
|
456
446
|
runtime,
|
|
457
447
|
payload,
|
|
@@ -590,6 +580,10 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
590
580
|
private mcpCleanup: Promise<void> = Promise.resolve();
|
|
591
581
|
private removeExternalToolHandler: (() => void) | null = null;
|
|
592
582
|
private removeControlRequestHandler: (() => void) | null = null;
|
|
583
|
+
private inheritedTools: InheritedToolSource | null = null;
|
|
584
|
+
private removeInheritedListener: (() => void) | null = null;
|
|
585
|
+
/** This session's own selection, after MCP tools and wildcards resolve. */
|
|
586
|
+
private ownSelection: SessionToolSelection | null = null;
|
|
593
587
|
|
|
594
588
|
constructor(
|
|
595
589
|
private readonly remoteOptions: AppServerSessionOptions,
|
|
@@ -605,6 +599,34 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
605
599
|
}
|
|
606
600
|
}
|
|
607
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Use the tools and policy of the `registerTools()` registration covering
|
|
604
|
+
* this conversation, and follow its updates while the session is open.
|
|
605
|
+
*/
|
|
606
|
+
inheritTools(source: InheritedToolSource): void {
|
|
607
|
+
this.inheritedTools = source;
|
|
608
|
+
this.removeInheritedListener = source.onChange(() => {
|
|
609
|
+
void this.refreshInheritedTools().catch(() => undefined);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Rebuild the effective tool map in place; call handlers hold the map. */
|
|
614
|
+
private applyToolSelection(): SessionToolSelection {
|
|
615
|
+
const selection = mergeInheritedTools(
|
|
616
|
+
this.inheritedTools?.current() ?? null,
|
|
617
|
+
this.ownSelection ?? { tools: [] },
|
|
618
|
+
);
|
|
619
|
+
this.externalTools.clear();
|
|
620
|
+
for (const tool of selection.tools) this.externalTools.set(tool.name, tool);
|
|
621
|
+
return selection;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private async refreshInheritedTools(): Promise<void> {
|
|
625
|
+
if (!this.ownSelection || !this.runtime || !(this.controller instanceof AppServerRuntimeController)) return;
|
|
626
|
+
const selection = this.applyToolSelection();
|
|
627
|
+
await this.controller.updateToolSelection(this.runtime, selection);
|
|
628
|
+
}
|
|
629
|
+
|
|
608
630
|
protected override shouldEnableMemfs(options: LettaCodeClientSessionOptions | CreateAgentOptions): boolean {
|
|
609
631
|
if (this.mode.kind !== "create-agent") return false;
|
|
610
632
|
return (options as CreateAgentOptions).memfs !== false;
|
|
@@ -627,6 +649,8 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
627
649
|
}));
|
|
628
650
|
|
|
629
651
|
const options = this.currentOptions();
|
|
652
|
+
this.externalTools.clear();
|
|
653
|
+
for (const tool of this.mode.options.tools ?? []) this.externalTools.set(tool.name, tool);
|
|
630
654
|
this.mcpBridge = await connectMcpServers(
|
|
631
655
|
"mcpServers" in options ? options.mcpServers : undefined,
|
|
632
656
|
{
|
|
@@ -637,6 +661,15 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
637
661
|
for (const tool of this.mcpBridge.tools) {
|
|
638
662
|
this.externalTools.set(tool.name, tool);
|
|
639
663
|
}
|
|
664
|
+
const mcpToolNames = this.mcpBridge.tools.map((tool) => tool.name);
|
|
665
|
+
const ownToolset = "toolset" in options ? options.toolset : undefined;
|
|
666
|
+
const ownAllowedTools = expandMcpToolWildcards(options.allowedTools, mcpToolNames);
|
|
667
|
+
this.ownSelection = {
|
|
668
|
+
tools: [...this.externalTools.values()],
|
|
669
|
+
...(ownAllowedTools !== undefined ? { allowedTools: ownAllowedTools } : {}),
|
|
670
|
+
...(ownToolset !== undefined ? { toolset: ownToolset } : {}),
|
|
671
|
+
};
|
|
672
|
+
const { allowedTools, toolset: clientToolset } = this.applyToolSelection();
|
|
640
673
|
|
|
641
674
|
this.removeControlRequestHandler = registerAppServerControlRequestHandler({
|
|
642
675
|
client,
|
|
@@ -644,9 +677,9 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
644
677
|
getOptions: () => this.currentOptions(),
|
|
645
678
|
});
|
|
646
679
|
|
|
647
|
-
if (this.externalTools.size > 0) {
|
|
680
|
+
if (this.externalTools.size > 0 || this.inheritedTools) {
|
|
648
681
|
this.removeExternalToolHandler = client.onExternalToolCall(
|
|
649
|
-
createExternalToolCallHandler(this.externalTools),
|
|
682
|
+
createExternalToolCallHandler(this.externalTools, () => this.runtime),
|
|
650
683
|
);
|
|
651
684
|
}
|
|
652
685
|
|
|
@@ -667,21 +700,19 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
667
700
|
);
|
|
668
701
|
}
|
|
669
702
|
}
|
|
670
|
-
const response = await this.startRuntime(
|
|
703
|
+
const response = await this.startRuntime(
|
|
704
|
+
client,
|
|
705
|
+
runtimeToolPolicy(allowedTools, clientToolset),
|
|
706
|
+
);
|
|
671
707
|
if (!response.success || !response.runtime) {
|
|
672
708
|
throw new Error(response.error ?? "Failed to start app-server runtime");
|
|
673
709
|
}
|
|
674
710
|
|
|
675
711
|
this.watchTransportDisconnect(client);
|
|
712
|
+
const toolPolicyApplied = await supportsRuntimeToolPolicy(client);
|
|
676
713
|
|
|
677
714
|
const tools = agentToolNames(response.agent);
|
|
678
715
|
const skillSources = options.skillSources;
|
|
679
|
-
const clientToolset = "toolset" in options ? options.toolset : undefined;
|
|
680
|
-
const mcpToolNames = this.mcpBridge?.tools.map((tool) => tool.name) ?? [];
|
|
681
|
-
const allowedTools = expandMcpToolWildcards(
|
|
682
|
-
options.allowedTools,
|
|
683
|
-
mcpToolNames,
|
|
684
|
-
);
|
|
685
716
|
const availableTools =
|
|
686
717
|
tools === undefined && mcpToolNames.length === 0
|
|
687
718
|
? undefined
|
|
@@ -692,6 +723,7 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
692
723
|
this.remoteOptions,
|
|
693
724
|
allowedTools,
|
|
694
725
|
clientToolset,
|
|
726
|
+
toolPolicyApplied,
|
|
695
727
|
),
|
|
696
728
|
runtime: response.runtime,
|
|
697
729
|
model:
|
|
@@ -719,6 +751,10 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
719
751
|
}
|
|
720
752
|
|
|
721
753
|
protected override onCoreClose(): void {
|
|
754
|
+
if (this.closed) {
|
|
755
|
+
this.removeInheritedListener?.();
|
|
756
|
+
this.removeInheritedListener = null;
|
|
757
|
+
}
|
|
722
758
|
this.removeExternalToolHandler?.();
|
|
723
759
|
this.removeExternalToolHandler = null;
|
|
724
760
|
this.removeControlRequestHandler?.();
|
|
@@ -756,12 +792,16 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
756
792
|
|
|
757
793
|
private async startRuntime(
|
|
758
794
|
client: AppServerClient,
|
|
795
|
+
toolPolicy: RuntimeToolPolicy,
|
|
759
796
|
): Promise<RuntimeStartResponseMessage> {
|
|
760
|
-
const command = await this.buildRuntimeStartCommand(client);
|
|
797
|
+
const command = await this.buildRuntimeStartCommand(client, toolPolicy);
|
|
761
798
|
return client.runtimeStart(command);
|
|
762
799
|
}
|
|
763
800
|
|
|
764
|
-
private async buildRuntimeStartCommand(
|
|
801
|
+
private async buildRuntimeStartCommand(
|
|
802
|
+
client: AppServerClient,
|
|
803
|
+
toolPolicy: RuntimeToolPolicy,
|
|
804
|
+
): Promise<RuntimeStartCommand> {
|
|
765
805
|
const options = this.mode.options;
|
|
766
806
|
const command: Record<string, unknown> = {
|
|
767
807
|
client_info: {
|
|
@@ -799,6 +839,10 @@ export class AppServerSession extends RemoteClientSessionCore {
|
|
|
799
839
|
}
|
|
800
840
|
const groups = externalToolGroups([...this.externalTools.values()]);
|
|
801
841
|
if (groups) command.external_tools = groups;
|
|
842
|
+
// Sent on create, resume and every reconnect: the listener keeps it only
|
|
843
|
+
// while this connection is open, and applies it to every turn, including
|
|
844
|
+
// task notifications and cron prompts the runtime starts itself.
|
|
845
|
+
command.tool_policy = toolPolicy;
|
|
802
846
|
|
|
803
847
|
if (this.mode.kind === "create-agent") {
|
|
804
848
|
command.create_agent = {
|
package/src/client-base.ts
CHANGED
|
@@ -33,6 +33,11 @@ import type {
|
|
|
33
33
|
ModelsClient,
|
|
34
34
|
} from "./management-types.js";
|
|
35
35
|
import { createQuery } from "./query.js";
|
|
36
|
+
import {
|
|
37
|
+
ToolRegistry,
|
|
38
|
+
type RegisterToolsOptions,
|
|
39
|
+
type ToolRegistration,
|
|
40
|
+
} from "./tool-registration.js";
|
|
36
41
|
import type {
|
|
37
42
|
Query,
|
|
38
43
|
QueryOptions,
|
|
@@ -161,6 +166,7 @@ export class LettaAgentClientBase implements AsyncDisposable {
|
|
|
161
166
|
private agentRepositoriesClient: AgentRepositoriesClient | null = null;
|
|
162
167
|
private cloudClient: Letta | null = null;
|
|
163
168
|
private managementTransport: ManagementTransport | null = null;
|
|
169
|
+
private toolRegistry: ToolRegistry | null = null;
|
|
164
170
|
private closed = false;
|
|
165
171
|
private closePromise: Promise<void> | null = null;
|
|
166
172
|
|
|
@@ -351,6 +357,11 @@ export class LettaAgentClientBase implements AsyncDisposable {
|
|
|
351
357
|
*
|
|
352
358
|
* `options.environment` overrides the client's default execution target for
|
|
353
359
|
* Letta Cloud sessions. Remote app-server URLs already select the runtime.
|
|
360
|
+
*
|
|
361
|
+
* If `client.registerTools()` covers this conversation, the session uses the
|
|
362
|
+
* registration's tools, `allowedTools` and `toolset` and follows its
|
|
363
|
+
* updates. Session `tools` are added on top, and session `allowedTools` can
|
|
364
|
+
* only narrow the registration's.
|
|
354
365
|
*/
|
|
355
366
|
resumeSession(
|
|
356
367
|
id: string,
|
|
@@ -360,21 +371,25 @@ export class LettaAgentClientBase implements AsyncDisposable {
|
|
|
360
371
|
const sessionOptions = stripCloudExecutionOptions(options);
|
|
361
372
|
validateCreateSessionOptions(sessionOptions);
|
|
362
373
|
this.assertSessionBackend("resumeSession", options);
|
|
374
|
+
const inherited = this.backend === "cloud"
|
|
375
|
+
? null
|
|
376
|
+
: this.toolRegistry?.inheritFor(id, sessionOptions) ?? null;
|
|
363
377
|
|
|
364
378
|
if (this.backend === "remote") {
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
379
|
+
const session = looksLikeConversationId(id)
|
|
380
|
+
? new AppServerSession(this.appServerSessionOptions(), {
|
|
381
|
+
kind: "session",
|
|
382
|
+
conversationId: id,
|
|
383
|
+
options,
|
|
384
|
+
})
|
|
385
|
+
: new AppServerSession(this.appServerSessionOptions(), {
|
|
386
|
+
kind: "session",
|
|
387
|
+
agentId: id,
|
|
388
|
+
defaultConversation: true,
|
|
389
|
+
options,
|
|
390
|
+
});
|
|
391
|
+
if (inherited) session.inheritTools(inherited);
|
|
392
|
+
return session;
|
|
378
393
|
}
|
|
379
394
|
if (this.backend === "cloud") {
|
|
380
395
|
if (looksLikeConversationId(id)) {
|
|
@@ -399,7 +414,9 @@ export class LettaAgentClientBase implements AsyncDisposable {
|
|
|
399
414
|
this.getCloudClient(),
|
|
400
415
|
);
|
|
401
416
|
}
|
|
402
|
-
|
|
417
|
+
const session = this.resumeLocalSession(id, options);
|
|
418
|
+
if (inherited && session instanceof AppServerSession) session.inheritTools(inherited);
|
|
419
|
+
return session;
|
|
403
420
|
}
|
|
404
421
|
|
|
405
422
|
/**
|
|
@@ -740,6 +757,30 @@ export class LettaAgentClientBase implements AsyncDisposable {
|
|
|
740
757
|
return this.managementTransport;
|
|
741
758
|
}
|
|
742
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Register client tools for many conversations over one long-lived
|
|
762
|
+
* connection. Every turn in those conversations can call them, including
|
|
763
|
+
* turns the runtime starts itself, such as task notifications and cron
|
|
764
|
+
* prompts, with or without an open session.
|
|
765
|
+
*/
|
|
766
|
+
registerTools(options: RegisterToolsOptions): Promise<ToolRegistration> {
|
|
767
|
+
this.assertOpen();
|
|
768
|
+
if (this.backend === "cloud") {
|
|
769
|
+
return Promise.reject(new Error(
|
|
770
|
+
'registerTools() requires backend: "local" or "remote". Cloud sessions register tools through the tools session option.',
|
|
771
|
+
));
|
|
772
|
+
}
|
|
773
|
+
this.toolRegistry ??= new ToolRegistry(
|
|
774
|
+
this.backend === "remote" ? this.remoteOptions() : this.localAppServerConnection(),
|
|
775
|
+
);
|
|
776
|
+
return this.toolRegistry.register(options);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/** How the local backend reaches its App Server; overridden by the Node client. */
|
|
780
|
+
protected localAppServerConnection(): AppServerSessionOptions {
|
|
781
|
+
throw this.localBackendUnavailableError();
|
|
782
|
+
}
|
|
783
|
+
|
|
743
784
|
/**
|
|
744
785
|
* Release resources owned directly by this client, including its pooled
|
|
745
786
|
* management connection. The Node client also stops its shared local App
|
|
@@ -748,6 +789,8 @@ export class LettaAgentClientBase implements AsyncDisposable {
|
|
|
748
789
|
close(): Promise<void> {
|
|
749
790
|
if (this.closePromise) return this.closePromise;
|
|
750
791
|
this.closed = true;
|
|
792
|
+
this.toolRegistry?.close();
|
|
793
|
+
this.toolRegistry = null;
|
|
751
794
|
const transport = this.managementTransport;
|
|
752
795
|
this.managementTransport = null;
|
|
753
796
|
const close = transport?.close() ?? Promise.resolve();
|
package/src/client-entry.ts
CHANGED
|
@@ -26,6 +26,7 @@ export class LettaAgentClient extends LettaAgentClientBase {
|
|
|
26
26
|
export { CloudManagedSandboxExpiredError } from "./cloud-session.js";
|
|
27
27
|
export { ConversationForkHydrationError } from "./management-errors.js";
|
|
28
28
|
export { RepositoriesClient } from "./repositories.js";
|
|
29
|
+
export type { RegisterToolsOptions, ToolRegistration } from "./tool-registration.js";
|
|
29
30
|
export type * from "./sandbox-files.js";
|
|
30
31
|
export type {
|
|
31
32
|
Computer,
|
package/src/client.ts
CHANGED
|
@@ -42,11 +42,15 @@ export class LettaAgentClient extends LettaAgentClientBase {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
protected override createLocalManagementTransport(): ManagementTransport {
|
|
45
|
+
return new AppServerManagementTransport(this.localAppServerConnection());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
protected override localAppServerConnection() {
|
|
45
49
|
const localOptions = (
|
|
46
50
|
this.options as LettaCodeLocalClientOptions
|
|
47
51
|
).appServer;
|
|
48
52
|
const sharedServer = this.sharedLocalServer();
|
|
49
|
-
return
|
|
53
|
+
return {
|
|
50
54
|
...(localOptions?.url !== undefined
|
|
51
55
|
? { url: localOptions.url }
|
|
52
56
|
: {
|
|
@@ -58,7 +62,7 @@ export class LettaAgentClient extends LettaAgentClientBase {
|
|
|
58
62
|
...(localOptions?.requestTimeoutMs !== undefined
|
|
59
63
|
? { requestTimeoutMs: localOptions.requestTimeoutMs }
|
|
60
64
|
: {}),
|
|
61
|
-
}
|
|
65
|
+
};
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
protected override async createLocalAgent(
|
package/src/cloud-session.ts
CHANGED
|
@@ -14,16 +14,16 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
AppServerRuntimeController,
|
|
16
16
|
agentToolNames,
|
|
17
|
-
createExternalToolCallHandler,
|
|
18
|
-
externalToolGroups,
|
|
19
17
|
registerAppServerControlRequestHandler,
|
|
20
18
|
} from "./app-server-session.js";
|
|
19
|
+
import { createExternalToolCallHandler, externalToolGroups } from "./external-tools.js";
|
|
21
20
|
import { createCloudStatusTransportConstructor } from "./cloud-status-transport.js";
|
|
22
21
|
import {
|
|
23
22
|
connectMcpServers,
|
|
24
23
|
expandMcpToolWildcards,
|
|
25
24
|
type McpToolBridge,
|
|
26
25
|
} from "./mcp-runtime.js";
|
|
26
|
+
import { runtimeToolPolicy } from "./runtime-tool-policy.js";
|
|
27
27
|
import { CloudManagementTransport } from "./cloud-management.js";
|
|
28
28
|
import {
|
|
29
29
|
createCloudClient,
|
|
@@ -588,7 +588,7 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
588
588
|
});
|
|
589
589
|
if (this.externalTools.size > 0) {
|
|
590
590
|
this.removeExternalToolHandler = client.onExternalToolCall(
|
|
591
|
-
createExternalToolCallHandler(this.externalTools),
|
|
591
|
+
createExternalToolCallHandler(this.externalTools, () => this.runtime),
|
|
592
592
|
);
|
|
593
593
|
}
|
|
594
594
|
}
|
|
@@ -656,6 +656,15 @@ export class CloudEnvironmentSession extends RemoteClientSessionCore {
|
|
|
656
656
|
}
|
|
657
657
|
const groups = externalToolGroups([...this.externalTools.values()]);
|
|
658
658
|
if (groups) command.external_tools = groups;
|
|
659
|
+
// Re-sent on idle-transport recovery so a new connection restores it.
|
|
660
|
+
// Per-message fields are still sent: Cloud does not confirm the capability.
|
|
661
|
+
command.tool_policy = runtimeToolPolicy(
|
|
662
|
+
expandMcpToolWildcards(
|
|
663
|
+
options.allowedTools,
|
|
664
|
+
this.mcpBridge?.tools.map((tool) => tool.name) ?? [],
|
|
665
|
+
),
|
|
666
|
+
"toolset" in options ? options.toolset : undefined,
|
|
667
|
+
);
|
|
659
668
|
|
|
660
669
|
return (await client.runtimeStart(
|
|
661
670
|
command as Parameters<AppServerClient["runtimeStart"]>[0],
|
|
@@ -189,6 +189,10 @@ export class CloudStatusTransport implements AppServerSocketLike {
|
|
|
189
189
|
if (channel === "control" && message.type === "stream_delta") {
|
|
190
190
|
return false;
|
|
191
191
|
}
|
|
192
|
+
// Tool requests are answered on control; the stream copy is a duplicate.
|
|
193
|
+
if (channel === "stream" && message.type === "external_tool_call_request") {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
192
196
|
if (this.isDuplicate(message)) return false;
|
|
193
197
|
this.trackEventSequence(message);
|
|
194
198
|
return true;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { AppServerExternalToolCallHandler } from "@letta-ai/letta-code/app-server-client";
|
|
2
|
+
import type { RuntimeScope } from "./remote-session-protocol.js";
|
|
3
|
+
import type { AgentToolCallContext, AnyAgentTool } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/** The `external_tools` groups that advertise client tools to the runtime. */
|
|
6
|
+
export function externalToolGroups(tools: AnyAgentTool[] | undefined): Array<Record<string, unknown>> | undefined {
|
|
7
|
+
if (!tools || tools.length === 0) return undefined;
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
tools: tools.map((tool) => ({
|
|
11
|
+
name: tool.name,
|
|
12
|
+
label: tool.label,
|
|
13
|
+
description: tool.description,
|
|
14
|
+
parameters: tool.parameters as Record<string, unknown>,
|
|
15
|
+
})),
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MAX_REMEMBERED_EXTERNAL_TOOL_CALLS = 200;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Run a runtime's external tool call against the client tools by name.
|
|
24
|
+
* `fallbackRuntime` covers listeners that send calls without a `runtime`.
|
|
25
|
+
*/
|
|
26
|
+
export function createExternalToolCallHandler(
|
|
27
|
+
externalTools: Map<string, AnyAgentTool>,
|
|
28
|
+
fallbackRuntime?: () => RuntimeScope | null,
|
|
29
|
+
): AppServerExternalToolCallHandler {
|
|
30
|
+
// A request can be delivered more than once (e.g. relayed on more than one
|
|
31
|
+
// socket). Share one execution per request_id so a tool never runs twice.
|
|
32
|
+
const calls = new Map<string, ReturnType<AppServerExternalToolCallHandler>>();
|
|
33
|
+
const execute: AppServerExternalToolCallHandler = async (request) => {
|
|
34
|
+
const tool = externalTools.get(request.tool_name);
|
|
35
|
+
if (!tool) {
|
|
36
|
+
throw new Error(`Unknown external tool: ${request.tool_name}`);
|
|
37
|
+
}
|
|
38
|
+
const runtime = request.runtime ?? fallbackRuntime?.() ?? null;
|
|
39
|
+
const context: AgentToolCallContext | undefined = runtime
|
|
40
|
+
? { toolCallId: request.tool_call_id, agentId: runtime.agent_id, conversationId: runtime.conversation_id }
|
|
41
|
+
: undefined;
|
|
42
|
+
const result = await tool.execute(request.tool_call_id, request.input, undefined, undefined, context);
|
|
43
|
+
return {
|
|
44
|
+
content: result.content.map((part) => ({
|
|
45
|
+
type: part.type,
|
|
46
|
+
...(part.text !== undefined ? { text: part.text } : {}),
|
|
47
|
+
...(part.data !== undefined ? { data: part.data } : {}),
|
|
48
|
+
...(part.mimeType !== undefined ? { mimeType: part.mimeType } : {}),
|
|
49
|
+
})),
|
|
50
|
+
...(result.isError === true ? { is_error: true } : {}),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
return (request) => {
|
|
54
|
+
const requestId = request.request_id;
|
|
55
|
+
if (typeof requestId !== "string" || requestId.length === 0) {
|
|
56
|
+
return execute(request);
|
|
57
|
+
}
|
|
58
|
+
const existing = calls.get(requestId);
|
|
59
|
+
if (existing) return existing;
|
|
60
|
+
const call = execute(request);
|
|
61
|
+
calls.set(requestId, call);
|
|
62
|
+
while (calls.size > MAX_REMEMBERED_EXTERNAL_TOOL_CALLS) {
|
|
63
|
+
const oldest = calls.keys().next().value;
|
|
64
|
+
if (oldest === undefined) break;
|
|
65
|
+
calls.delete(oldest);
|
|
66
|
+
}
|
|
67
|
+
return call;
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -68,6 +68,7 @@ export type {
|
|
|
68
68
|
SDKToolResultMessage,
|
|
69
69
|
SDKReasoningMessage,
|
|
70
70
|
SDKResultMessage,
|
|
71
|
+
SDKTurnOrigin,
|
|
71
72
|
SDKErrorCode,
|
|
72
73
|
SDKStreamEventMessage,
|
|
73
74
|
SDKStreamEventPayload,
|
|
@@ -147,6 +148,7 @@ export type {
|
|
|
147
148
|
BootstrapStateResult,
|
|
148
149
|
// Tool types
|
|
149
150
|
AgentTool,
|
|
151
|
+
AgentToolCallContext,
|
|
150
152
|
AgentToolResult,
|
|
151
153
|
AgentToolResultContent,
|
|
152
154
|
AgentToolUpdateCallback,
|
|
@@ -190,6 +192,7 @@ export type {
|
|
|
190
192
|
} from "./management-types.js";
|
|
191
193
|
|
|
192
194
|
export { RepositoriesClient } from "./repositories.js";
|
|
195
|
+
export type { RegisterToolsOptions, ToolRegistration } from "./tool-registration.js";
|
|
193
196
|
export { ConversationForkHydrationError } from "./management-errors.js";
|
|
194
197
|
export type {
|
|
195
198
|
Computer,
|
|
@@ -289,9 +289,7 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
289
289
|
}
|
|
290
290
|
await this.send(message);
|
|
291
291
|
for await (const msg of this.stream()) {
|
|
292
|
-
if (msg.type === "result")
|
|
293
|
-
return msg;
|
|
294
|
-
}
|
|
292
|
+
if (msg.type === "result" && msg.origin === undefined) return msg;
|
|
295
293
|
}
|
|
296
294
|
return {
|
|
297
295
|
type: "result",
|
|
@@ -323,7 +321,8 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
323
321
|
const msg = await this.turns.nextMessage();
|
|
324
322
|
if (!msg) break;
|
|
325
323
|
yield msg;
|
|
326
|
-
|
|
324
|
+
// Results of turns the runtime started are yielded; only ours end the pass.
|
|
325
|
+
if (msg.type === "result" && msg.origin === undefined) break;
|
|
327
326
|
}
|
|
328
327
|
}
|
|
329
328
|
|
|
@@ -717,6 +716,7 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
717
716
|
this.controller?.close();
|
|
718
717
|
this.controller = null;
|
|
719
718
|
const error = new Error(`${this.label} connection closed before send`);
|
|
719
|
+
this.turns.failServerTurn(error.message);
|
|
720
720
|
for (const cancel of [...this.deviceStatusRefreshCancels]) cancel(error);
|
|
721
721
|
this.deviceStatusRefreshCancels.clear();
|
|
722
722
|
this.onIdleTransportDisconnect();
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
RecoverPendingApprovalsResult,
|
|
11
11
|
ReasoningEffort,
|
|
12
12
|
SDKErrorCode,
|
|
13
|
+
SDKTurnOrigin,
|
|
13
14
|
OutputFormat,
|
|
14
15
|
SDKQueueItem,
|
|
15
16
|
SendMessage,
|
|
@@ -151,6 +152,8 @@ export type TurnTracker = {
|
|
|
151
152
|
/** Caller-supplied OTID for this turn's user message, when one was provided. */
|
|
152
153
|
otid?: string;
|
|
153
154
|
outputFormat?: OutputFormat;
|
|
155
|
+
/** Set when the runtime started this turn rather than the session. */
|
|
156
|
+
origin?: SDKTurnOrigin;
|
|
154
157
|
queuedAt: number;
|
|
155
158
|
startedAt: number;
|
|
156
159
|
assistantText: string;
|