@letta-ai/letta-agent-sdk 0.8.34 → 0.8.36
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 +8 -2
- package/dist/client-base.d.ts +2 -2
- package/dist/client-entry.js.map +1 -1
- package/dist/client.d.ts +4 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +156 -38
- package/dist/index.js.map +9 -8
- package/dist/local-app-server-session.d.ts +2 -1
- package/dist/local-app-server-session.d.ts.map +1 -1
- package/dist/local-app-server.d.ts +1 -0
- package/dist/local-app-server.d.ts.map +1 -1
- package/dist/shared-local-app-server.d.ts +13 -0
- package/dist/shared-local-app-server.d.ts.map +1 -0
- package/dist/tests/fixtures/shared-app-server-client.d.ts +2 -0
- package/dist/tests/fixtures/shared-app-server-client.d.ts.map +1 -0
- package/dist/types.d.ts +5 -5
- package/package.json +1 -1
- package/src/client-base.ts +2 -2
- package/src/client.ts +39 -13
- package/src/index.ts +54 -7
- package/src/local-app-server-session.ts +6 -1
- package/src/local-app-server.ts +25 -14
- package/src/shared-local-app-server.ts +57 -0
- package/src/types.ts +5 -5
package/README.md
CHANGED
|
@@ -34,8 +34,14 @@ for await (const message of session.stream()) {
|
|
|
34
34
|
|
|
35
35
|
Local and remote management clients can use `await using client = new
|
|
36
36
|
LettaAgentClient(...)`, or call `await client.close()` explicitly. Client
|
|
37
|
-
disposal closes its pooled management connection and
|
|
38
|
-
|
|
37
|
+
disposal closes its pooled management connection and shared local App Server.
|
|
38
|
+
Local calls share one lazily started server per client; closing a session does
|
|
39
|
+
not stop its siblings. Keep the client open while using these sessions.
|
|
40
|
+
Sessions with environment overrides or memory confinement retain dedicated
|
|
41
|
+
processes and must be closed separately. Externally hosted servers are never
|
|
42
|
+
stopped by the client.
|
|
43
|
+
The top-level convenience functions own short-lived clients; use a
|
|
44
|
+
`LettaAgentClient` instance to share a server across repeated or concurrent calls.
|
|
39
45
|
|
|
40
46
|
Latency-sensitive applications can initialize the runtime and transport before
|
|
41
47
|
the first user action without fetching transcript history or invoking the model:
|
package/dist/client-base.d.ts
CHANGED
|
@@ -86,8 +86,8 @@ export declare class LettaAgentClientBase implements AsyncDisposable {
|
|
|
86
86
|
private getManagementTransport;
|
|
87
87
|
/**
|
|
88
88
|
* Release resources owned directly by this client, including its pooled
|
|
89
|
-
* management connection
|
|
90
|
-
*
|
|
89
|
+
* management connection. The Node client also stops its shared local App
|
|
90
|
+
* Server; dedicated and externally hosted sessions remain independently owned.
|
|
91
91
|
*/
|
|
92
92
|
close(): Promise<void>;
|
|
93
93
|
/** Async-disposal alias for close(). */
|
package/dist/client-entry.js.map
CHANGED
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"import type {\n AgentListParams,\n AgentUpdateParams,\n} from \"@letta-ai/letta-client/resources/agents/agents\";\nimport type {\n ConversationCreateParams,\n ConversationForkParams,\n ConversationListParams,\n ConversationUpdateParams,\n} from \"@letta-ai/letta-client/resources/conversations/conversations\";\nimport type { MessageListParams } from \"@letta-ai/letta-client/resources/conversations/messages\";\nimport type {\n AgentRepositoriesClient,\n AgentsClient,\n ConversationsClient,\n ConversationMessagesResult,\n CreateConversationOptions,\n EnqueueMessageOptions,\n EnqueueMessageResult,\n ForkConversationOptions,\n LettaAgent,\n LettaConversation,\n ListAgentsOptions,\n ListConversationsOptions,\n ModelsClient,\n UpdateAgentOptions,\n UpdateConversationOptions,\n ConversationMessagesOptions,\n} from \"./management-types.js\";\nimport type { ListModelsResult, SendMessage } from \"./types.js\";\n\nexport interface ManagementTransport {\n close(): Promise<void>;\n listAgents(query: AgentListParams): Promise<LettaAgent[]>;\n retrieveAgent(agentId: string): Promise<LettaAgent>;\n updateAgent(\n agentId: string,\n body: AgentUpdateParams,\n ): Promise<LettaAgent>;\n deleteAgent(agentId: string): Promise<void>;\n listModels(): Promise<ListModelsResult>;\n listConversations(\n query: ConversationListParams,\n ): Promise<LettaConversation[]>;\n retrieveConversation(\n conversationId: string,\n ): Promise<LettaConversation>;\n createConversation(\n body: ConversationCreateParams,\n ): Promise<LettaConversation>;\n updateConversation(\n conversationId: string,\n body: ConversationUpdateParams,\n ): Promise<LettaConversation>;\n forkConversation(\n conversationId: string,\n query: ConversationForkParams,\n ): Promise<LettaConversation>;\n listConversationMessages(\n conversationId: string,\n query: MessageListParams,\n ): Promise<ConversationMessagesResult>;\n /** Cloud only. Non-cloud transports reject with a backend error. */\n enqueueConversationMessage(\n conversationId: string,\n message: SendMessage,\n options: EnqueueMessageOptions,\n ): Promise<EnqueueMessageResult>;\n}\n\ntype TransportProvider = () => ManagementTransport;\n\nfunction assertNonEmptyId(value: string, name: string): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new Error(`Invalid ${name}. Expected a non-empty string.`);\n }\n}\n\nfunction agentListQuery(options: ListAgentsOptions): AgentListParams {\n const orderBy = options.orderBy === \"createdAt\"\n ? \"created_at\"\n : options.orderBy === \"lastRunCompletion\"\n ? \"last_run_completion\"\n : undefined;\n return {\n before: options.before,\n after: options.after,\n limit: options.limit,\n order: options.order,\n order_by: orderBy,\n query_text: options.query,\n name: options.name,\n tags: options.tags,\n match_all_tags: options.matchAllTags,\n include: options.include,\n };\n}\n\nfunction agentUpdateBody(options: UpdateAgentOptions): AgentUpdateParams {\n return {\n name: options.name,\n description: options.description,\n model: options.model,\n model_settings: options.modelSettings,\n system: options.system,\n tags: options.tags,\n hidden: options.hidden,\n context_window_limit: options.contextWindowLimit,\n };\n}\n\nfunction conversationListQuery(\n options: ListConversationsOptions,\n): ConversationListParams {\n const orderBy: ConversationListParams[\"order_by\"] =\n options.orderBy === \"createdAt\"\n ? \"created_at\"\n : options.orderBy === \"lastRunCompletion\"\n ? \"last_run_completion\"\n : options.orderBy === \"lastMessageAt\"\n ? \"last_message_at\"\n : undefined;\n return {\n agent_id: options.agentId,\n after: options.after,\n limit: options.limit,\n order: options.order,\n order_by: orderBy,\n archive_status: options.archiveStatus,\n summary_search: options.summarySearch,\n };\n}\n\nfunction conversationCreateBody(\n options: CreateConversationOptions,\n): ConversationCreateParams {\n return {\n agent_id: options.agentId,\n summary: options.summary,\n description: options.description,\n model: options.model,\n model_settings: options.modelSettings,\n context_window_limit: options.contextWindowLimit,\n hidden: options.hidden,\n };\n}\n\nfunction conversationUpdateBody(\n options: UpdateConversationOptions,\n): ConversationUpdateParams {\n return {\n summary: options.summary,\n description: options.description,\n model: options.model,\n model_settings: options.modelSettings,\n context_window_limit: options.contextWindowLimit,\n archived: options.archived,\n };\n}\n\nfunction conversationForkQuery(\n options: ForkConversationOptions,\n): ConversationForkParams {\n return {\n message_id: options.messageId,\n hidden: options.hidden,\n };\n}\n\nfunction conversationMessagesQuery(\n options: ConversationMessagesOptions,\n): MessageListParams {\n return {\n before: options.before,\n after: options.after,\n order: options.order,\n limit: options.limit,\n };\n}\n\nexport function createAgentsClient(\n transport: TransportProvider,\n repositories: () => AgentRepositoriesClient,\n): AgentsClient {\n return {\n get repositories() {\n return repositories();\n },\n list: (options = {}) =>\n transport().listAgents(agentListQuery(options)),\n retrieve: (agentId) => transport().retrieveAgent(agentId),\n update: (agentId, options) =>\n transport().updateAgent(agentId, agentUpdateBody(options)),\n delete: async (agentId) => {\n assertNonEmptyId(agentId, \"agent id\");\n await transport().deleteAgent(agentId);\n },\n };\n}\n\nexport function createModelsClient(\n transport: TransportProvider,\n): ModelsClient {\n return {\n list: () => transport().listModels(),\n };\n}\n\nexport function createConversationsClient(\n transport: TransportProvider,\n): ConversationsClient {\n return {\n list: (options = {}) =>\n transport().listConversations(conversationListQuery(options)),\n retrieve: (conversationId) =>\n transport().retrieveConversation(conversationId),\n create: (options) =>\n transport().createConversation(conversationCreateBody(options)),\n update: (conversationId, options) =>\n transport().updateConversation(\n conversationId,\n conversationUpdateBody(options),\n ),\n fork: async (sourceConversationId, options = {}) => {\n assertNonEmptyId(sourceConversationId, \"conversation id\");\n if (options.messageId !== undefined) {\n assertNonEmptyId(options.messageId, \"message id\");\n }\n return await transport().forkConversation(\n sourceConversationId,\n conversationForkQuery(options),\n );\n },\n listMessages: (conversationId, options = {}) =>\n transport().listConversationMessages(\n conversationId,\n conversationMessagesQuery(options),\n ),\n enqueue: async (conversationId, message, options = {}) => {\n assertNonEmptyId(conversationId, \"conversation id\");\n if (conversationId.trim() === \"default\" && !options.agentId) {\n throw new Error(\n 'enqueue(\"default\", ...) requires options.agentId to identify the agent.',\n );\n }\n if (options.agentId !== undefined) {\n assertNonEmptyId(options.agentId, \"agent id\");\n }\n if (options.clientMessageId !== undefined) {\n assertNonEmptyId(options.clientMessageId, \"client message id\");\n }\n return await transport().enqueueConversationMessage(\n conversationId,\n message,\n options,\n );\n },\n };\n}\n",
|
|
168
168
|
"import type { Query, QueryOptions, QueryParams } from \"./query-types.js\";\nimport type { LettaCodeSession, SDKMessage } from \"./types.js\";\n\ntype CreateQuerySession = (options: QueryOptions) => Promise<LettaCodeSession>;\n\n/** Create one ordinary conversation and stream its query result. */\nexport function createQuery(\n createSession: CreateQuerySession,\n params: QueryParams,\n): Query {\n let session: LettaCodeSession | null = null;\n let closed = false;\n let conversationId: string | null = null;\n let agentId: string | null = null;\n\n function closeSession(): void {\n conversationId = session?.conversationId ?? conversationId;\n agentId = session?.agentId ?? agentId;\n session?.close();\n }\n\n const iterator = (async function* runQuery(): AsyncGenerator<\n SDKMessage,\n void,\n unknown\n > {\n try {\n session = await createSession(params.options);\n if (closed) return;\n await session.send(params.prompt);\n yield* session.stream();\n } finally {\n closed = true;\n closeSession();\n }\n })();\n\n const query = Object.assign(iterator, {\n async interrupt(): Promise<void> {\n await session?.abort();\n },\n close(): void {\n if (closed) return;\n closed = true;\n closeSession();\n void iterator.return();\n },\n });\n return Object.defineProperties(query, {\n conversationId: {\n get: () => session?.conversationId ?? conversationId,\n enumerable: true,\n },\n agentId: {\n get: () => session?.agentId ?? agentId,\n enumerable: true,\n },\n }) as Query;\n}\n",
|
|
169
169
|
"/**\n * SDK Validation\n *\n * Validates user-provided options before spawning the CLI.\n */\n\nimport type { \n CreateSessionOptions,\n CreateAgentOptions,\n MemoryItem, \n McpServers,\n CreateBlock,\n SystemPromptPreset,\n SkillSource,\n DreamingOptions,\n} from \"./types.js\";\n\nconst VALID_SKILL_SOURCES: SkillSource[] = [\n \"bundled\",\n \"global\",\n \"agent\",\n \"project\",\n];\n\nconst VALID_TOOLSET_BASES = [\n \"auto\",\n \"codex\",\n \"codex_snake\",\n \"default\",\n \"gemini\",\n \"gemini_snake\",\n \"none\",\n] as const;\n\nfunction validateOutputFormat(outputFormat: CreateSessionOptions[\"outputFormat\"]): void {\n if (outputFormat === undefined) return;\n if (\n !outputFormat ||\n typeof outputFormat !== \"object\" ||\n outputFormat.type !== \"json_schema\" ||\n !outputFormat.schema ||\n typeof outputFormat.schema !== \"object\" ||\n Array.isArray(outputFormat.schema)\n ) {\n throw new Error(\n \"Invalid outputFormat. Expected { type: 'json_schema', schema: <JSON Schema object> }.\",\n );\n }\n if (outputFormat.maxRetries !== undefined &&\n (!Number.isInteger(outputFormat.maxRetries) || outputFormat.maxRetries < 0 || outputFormat.maxRetries > 5)) {\n throw new Error(\"Invalid outputFormat.maxRetries. Expected an integer from 0 to 5.\");\n }\n}\n\nconst VALID_REASONING_EFFORTS = [\n \"none\",\n \"minimal\",\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n] as const;\n\n/**\n * Extract block labels from memory items.\n */\nfunction getBlockLabels(memory: MemoryItem[]): string[] {\n return memory\n .map((item) => {\n if (typeof item === \"string\") return item; // preset name\n if (\"label\" in item) return (item as CreateBlock).label; // CreateBlock\n return null; // blockId - no label to check\n })\n .filter((label): label is string => label !== null);\n}\n\nfunction validateRemovedSessionOptions(options: CreateSessionOptions): void {\n const removedOptions = options as CreateSessionOptions & Record<string, unknown>;\n if (removedOptions.systemPrompt !== undefined) {\n throw new Error(\"systemPrompt is not supported when opening an existing agent session.\");\n }\n if (removedOptions.disallowedTools !== undefined) {\n throw new Error(\"disallowedTools is not supported when opening an existing agent session.\");\n }\n if (removedOptions.systemInfoReminder !== undefined) {\n throw new Error(\"systemInfoReminder is not supported when opening an existing agent session.\");\n }\n if (removedOptions.includePartialMessages !== undefined) {\n throw new Error(\"includePartialMessages is not supported by app-server sessions.\");\n }\n if (\n options.dreaming &&\n \"behavior\" in options.dreaming &&\n options.dreaming.behavior !== undefined\n ) {\n throw new Error(\"dreaming.behavior is not supported when opening an existing agent session.\");\n }\n if (removedOptions.memfsStartup !== undefined) {\n throw new Error(\n \"memfsStartup is not supported by the SDK.\",\n );\n }\n}\n\n/**\n * Validate systemPrompt preset value.\n */\nfunction validateSystemPromptPreset(preset: string): void {\n const validPresets = [\n \"default\",\n \"letta-claude\",\n \"letta-codex\",\n \"letta-gemini\",\n \"claude\",\n \"codex\",\n \"gemini\",\n ];\n if (!validPresets.includes(preset)) {\n throw new Error(\n `Invalid system prompt preset '${preset}'. ` +\n `Valid presets: ${validPresets.join(\", \")}`\n );\n }\n}\n\nfunction validateClientToolset(\n toolset: CreateSessionOptions[\"toolset\"],\n): void {\n if (toolset === undefined) return;\n if (!toolset || typeof toolset !== \"object\" || Array.isArray(toolset)) {\n throw new Error(\"Invalid toolset. Expected an object with optional base and include fields.\");\n }\n if (\n toolset.base !== undefined &&\n !VALID_TOOLSET_BASES.includes(toolset.base)\n ) {\n throw new Error(\n `Invalid toolset.base '${String(toolset.base)}'. Valid values: ${VALID_TOOLSET_BASES.join(\", \")}`,\n );\n }\n if (\n toolset.include !== undefined &&\n (!Array.isArray(toolset.include) ||\n toolset.include.some(\n (toolName) => typeof toolName !== \"string\" || toolName.length === 0,\n ))\n ) {\n throw new Error(\n \"Invalid toolset.include. Expected an array of non-empty bundled tool names.\",\n );\n }\n}\n\nfunction validateSkillSources(sources: SkillSource[] | undefined): void {\n if (sources === undefined) {\n return;\n }\n\n for (const source of sources) {\n if (!VALID_SKILL_SOURCES.includes(source)) {\n throw new Error(\n `Invalid skill source '${source}'. Valid values: ${VALID_SKILL_SOURCES.join(\", \")}`\n );\n }\n }\n}\n\nfunction validateDreamingOptions(dreaming: DreamingOptions | undefined): void {\n if (dreaming === undefined) {\n return;\n }\n\n if (\n dreaming.trigger !== undefined &&\n ![\"off\", \"step-count\", \"compaction-event\"].includes(dreaming.trigger)\n ) {\n throw new Error(\n `Invalid dreaming.trigger '${String(dreaming.trigger)}'. Valid values: off, step-count, compaction-event`\n );\n }\n\n if (\n dreaming.behavior !== undefined &&\n ![\"reminder\", \"auto-launch\"].includes(dreaming.behavior)\n ) {\n throw new Error(\n `Invalid dreaming.behavior '${String(dreaming.behavior)}'. Valid values: reminder, auto-launch`\n );\n }\n\n if (\n dreaming.stepCount !== undefined &&\n (!Number.isInteger(dreaming.stepCount) || dreaming.stepCount <= 0)\n ) {\n throw new Error(\n \"Invalid dreaming.stepCount. Expected a positive integer.\"\n );\n }\n}\n\nfunction validateReasoningEffort(value: unknown): void {\n if (value === undefined) return;\n if (\n typeof value !== \"string\" ||\n !VALID_REASONING_EFFORTS.includes(value as (typeof VALID_REASONING_EFFORTS)[number])\n ) {\n throw new Error(\n `Invalid reasoningEffort '${String(value)}'. Valid values: ${VALID_REASONING_EFFORTS.join(\", \")}`\n );\n }\n}\n\nfunction validateMcpServers(servers: McpServers | undefined): void {\n if (servers === undefined) return;\n if (!servers || typeof servers !== \"object\" || Array.isArray(servers)) {\n throw new Error(\"Invalid mcpServers. Expected an object keyed by server name.\");\n }\n for (const [name, config] of Object.entries(servers)) {\n if (name.length === 0) {\n throw new Error(\"Invalid mcpServers. Server names must be non-empty.\");\n }\n if (!config || typeof config !== \"object\" || Array.isArray(config)) {\n throw new Error(`Invalid MCP server '${name}'. Expected a configuration object.`);\n }\n if (config.type === \"http\" || config.type === \"sse\") {\n if (typeof config.url !== \"string\" || config.url.length === 0) {\n throw new Error(`Invalid MCP server '${name}'. Expected a non-empty url.`);\n }\n continue;\n }\n if (config.type !== undefined && config.type !== \"stdio\") {\n throw new Error(\n `Invalid MCP server '${name}' type '${String(config.type)}'. Valid values: stdio, http, sse.`,\n );\n }\n if (typeof config.command !== \"string\" || config.command.length === 0) {\n throw new Error(`Invalid MCP server '${name}'. Expected a non-empty command.`);\n }\n }\n}\n\n/**\n * Validate CreateSessionOptions (used by createSession and resumeSession).\n */\nexport function validateCreateSessionOptions(options: CreateSessionOptions): void {\n if (options.stateless !== undefined && typeof options.stateless !== \"boolean\") {\n throw new Error(\"Invalid stateless. Expected a boolean.\");\n }\n if (options.stateless) {\n const persistedOption = [\n [\"model\", options.model],\n [\"reasoningEffort\", options.reasoningEffort],\n [\"dreaming\", options.dreaming],\n [\"resources\", options.resources],\n ].find(([, value]) => value !== undefined);\n if (persistedOption) {\n throw new Error(\n `stateless sessions cannot set ${persistedOption[0]} because it changes persistent configuration.`,\n );\n }\n }\n validateClientToolset(options.toolset);\n validateSkillSources(options.skillSources);\n validateMcpServers(options.mcpServers);\n validateReasoningEffort(options.reasoningEffort);\n validateDreamingOptions(options.dreaming);\n validateOutputFormat(options.outputFormat);\n validateRemovedSessionOptions(options);\n}\n\n/**\n * Validate CreateAgentOptions (used by createAgent).\n */\nexport function validateCreateAgentOptions(options: CreateAgentOptions): void {\n // Validate memory/persona consistency\n if (options.memory !== undefined) {\n const blockLabels = getBlockLabels(options.memory);\n\n if (options.persona !== undefined && !blockLabels.includes(\"persona\")) {\n throw new Error(\n \"Cannot set 'persona' value - block not included in 'memory'. \" +\n \"Either add 'persona' to memory array or remove the persona option.\"\n );\n }\n\n if (options.human !== undefined && !blockLabels.includes(\"human\")) {\n throw new Error(\n \"Cannot set 'human' value - block not included in 'memory'. \" +\n \"Either add 'human' to memory array or remove the human option.\"\n );\n }\n }\n\n // Validate systemPrompt preset if provided as preset object\n if (\n options.systemPrompt !== undefined &&\n typeof options.systemPrompt === \"object\"\n ) {\n validateSystemPromptPreset(options.systemPrompt.preset);\n } else if (\n options.systemPrompt !== undefined &&\n typeof options.systemPrompt === \"string\"\n ) {\n // Check if it's a preset name (if so, validate it)\n const validPresets = [\n \"default\",\n \"letta-claude\",\n \"letta-codex\",\n \"letta-gemini\",\n \"claude\",\n \"codex\",\n \"gemini\",\n ] as const;\n if (validPresets.includes(options.systemPrompt as SystemPromptPreset)) {\n validateSystemPromptPreset(options.systemPrompt);\n }\n // If not a preset, it's a custom string - no validation needed\n }\n\n validateSkillSources(options.skillSources);\n validateDreamingOptions(options.dreaming);\n}\n",
|
|
170
|
-
"import type Letta from \"@letta-ai/letta-client\";\nimport { RepositoriesClient } from \"./repositories.js\";\nimport { resolveSkillItems, type SkillNodeSupport } from \"./skill-loading.js\";\nimport { createAgentRepositoriesClient } from \"./agent-repositories.js\";\nimport { AppServerManagementTransport } from \"./app-server-management.js\";\nimport {\n AppServerSession,\n type AppServerSessionOptions,\n} from \"./app-server-session.js\";\nimport { CloudManagementTransport } from \"./cloud-management.js\";\nimport { createCloudClient } from \"./cloud-client.js\";\nimport {\n ComputersClientImpl,\n type ComputerSelector,\n type ComputersClient,\n} from \"./computers.js\";\nimport {\n CloudEnvironmentSession,\n assertCloudSessionOptionsSupported,\n createCloudAgent,\n validateCloudClientOptions,\n} from \"./cloud-session.js\";\nimport {\n createAgentsClient,\n createConversationsClient,\n createModelsClient,\n type ManagementTransport,\n} from \"./management.js\";\nimport type {\n AgentRepositoriesClient,\n AgentsClient,\n ConversationsClient,\n ModelsClient,\n} from \"./management-types.js\";\nimport { createQuery } from \"./query.js\";\nimport type {\n Query,\n QueryOptions,\n QueryParams,\n} from \"./query-types.js\";\nimport type {\n CreateAgentOptions,\n CreateSessionOptions,\n LettaCodeBackend,\n LettaCodeClientOptions,\n LettaCodeClientSessionOptions,\n LettaCodeEnvironment,\n LettaCodeRemoteClientOptions,\n LettaCodeLocalClientOptions,\n LettaCodeCloudClientOptions,\n LettaCodeSession,\n SDKResultMessage,\n SendMessage,\n} from \"./types.js\";\nimport {\n validateCreateAgentOptions,\n validateCreateSessionOptions,\n} from \"./validation.js\";\n\nconst VALID_BACKENDS = new Set<LettaCodeBackend>([\n \"local\",\n \"remote\",\n \"cloud\",\n]);\n\nfunction isLettaCodeBackend(value: string): value is LettaCodeBackend {\n return VALID_BACKENDS.has(value as LettaCodeBackend);\n}\n\nfunction getOptionsComputer(\n options: LettaCodeClientOptions,\n): ComputerSelector | undefined {\n if (!(\"computer\" in options) && !(\"environment\" in options)) return undefined;\n if (options.computer !== undefined && options.environment !== undefined) {\n throw new Error('Specify either \"computer\" or deprecated \"environment\", not both.');\n }\n return options.computer ?? options.environment;\n}\n\nfunction stripCloudExecutionOptions(\n options: LettaCodeClientSessionOptions,\n): CreateSessionOptions {\n const sessionOptions = { ...options };\n delete sessionOptions.computer;\n delete sessionOptions.environment;\n delete sessionOptions.sandbox;\n delete sessionOptions.filesystemConfinement;\n return sessionOptions;\n}\n\nfunction hasRepositoryResources(options: LettaCodeClientSessionOptions): boolean {\n return options.resources !== undefined && options.resources.length > 0;\n}\n\nfunction hasCreateAgentEnvironment(options: CreateAgentOptions): boolean {\n return \"environment\" in (options as Record<string, unknown>);\n}\n\nfunction looksLikeConversationId(id: string): boolean {\n return id.startsWith(\"conv-\") || id.startsWith(\"local-conv-\");\n}\n\nfunction querySessionOptions(\n options: QueryOptions,\n): LettaCodeClientSessionOptions {\n const {\n system: _system,\n parentAgentId: _parentAgentId,\n name: _name,\n isSubagent: _isSubagent,\n modelSettings: _modelSettings,\n contextWindowLimit: _contextWindowLimit,\n conversationId: _conversationId,\n disableMemoryGuard: _disableMemoryGuard,\n ...sessionOptions\n } = options;\n return sessionOptions;\n}\n\nfunction validateQueryOptions(options: QueryOptions): void {\n if (\n options.conversationId !== undefined &&\n (typeof options.conversationId !== \"string\" || !options.conversationId.trim())\n ) {\n throw new Error(\"query() conversationId must be a non-empty string.\");\n }\n if (typeof options.model !== \"string\" || options.model.length === 0) {\n throw new Error(\"query() requires a non-empty model.\");\n }\n if (typeof options.system !== \"string\") {\n throw new Error(\"query() requires a system prompt.\");\n }\n validateCreateSessionOptions(querySessionOptions(options));\n}\n\ntype OneShotSession = LettaCodeSession & {\n sendAndWaitForResult(message: SendMessage): Promise<SDKResultMessage>;\n};\n\n/**\n * Top-level Letta Agent SDK client.\n *\n * `backend` selects how the SDK reaches or runs the Letta Code harness.\n * `local` spawns an SDK-owned Letta Code app-server and speaks the websocket\n * protocol.\n * `remote` connects to a user-managed Letta Code app-server websocket endpoint.\n * `cloud` uses agents hosted on Letta Cloud, with an explicit remote\n * environment or SDK-managed sandbox.\n */\nexport class LettaAgentClientBase implements AsyncDisposable {\n readonly backend: LettaCodeBackend;\n readonly computer: ComputerSelector | undefined;\n /** @deprecated Use `computer`. */\n readonly environment: LettaCodeEnvironment | undefined;\n readonly agents: AgentsClient;\n readonly conversations: ConversationsClient;\n readonly models: ModelsClient;\n protected readonly options: LettaCodeClientOptions;\n private repositoriesClient: RepositoriesClient | null = null;\n private computersClient: ComputersClientImpl | null = null;\n private agentRepositoriesClient: AgentRepositoriesClient | null = null;\n private cloudClient: Letta | null = null;\n private managementTransport: ManagementTransport | null = null;\n private closed = false;\n private closePromise: Promise<void> | null = null;\n\n constructor(options: LettaCodeClientOptions = {}) {\n const backend = options.backend ?? \"local\";\n if (!isLettaCodeBackend(backend)) {\n throw new Error(\n `Invalid Letta Code backend '${String(backend)}'. Valid values: local, remote, cloud.`,\n );\n }\n\n this.backend = backend;\n this.computer = getOptionsComputer(options);\n this.environment = this.computer;\n this.options = options;\n this.agents = createAgentsClient(\n () => this.getManagementTransport(),\n () => this.getAgentRepositoriesClient(),\n );\n this.conversations = createConversationsClient(\n () => this.getManagementTransport(),\n );\n this.models = createModelsClient(() => this.getManagementTransport());\n\n if (this.backend === \"local\" && this.computer !== undefined) {\n const field = \"computer\" in options && options.computer !== undefined\n ? \"computer\"\n : \"environment\";\n throw new Error(\n `LettaAgentClient ${field} is only valid with backend: \"cloud\".`,\n );\n }\n if (this.backend === \"remote\" && this.computer !== undefined) {\n const field = \"computer\" in options && options.computer !== undefined\n ? \"computer\"\n : \"environment\";\n throw new Error(\n `LettaAgentClient ${field} is only valid with backend: \"cloud\"; remote url selects the app-server runtime.`,\n );\n }\n if (this.backend !== \"cloud\" && (options as { sandbox?: unknown }).sandbox !== undefined) {\n throw new Error('LettaAgentClient sandbox options are only valid with backend: \"cloud\".');\n }\n\n if (this.backend === \"local\") {\n const localOptions = options as LettaCodeLocalClientOptions;\n if (\"transport\" in localOptions) {\n throw new Error(\n 'Local transport selection has been removed. The local backend always uses the app-server protocol.',\n );\n }\n const requestTimeoutMs = localOptions.appServer?.requestTimeoutMs;\n if (\n requestTimeoutMs !== undefined &&\n (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0)\n ) {\n throw new Error(\"Invalid appServer.requestTimeoutMs. Expected a positive integer.\");\n }\n const startupTimeoutMs = localOptions.appServer?.startupTimeoutMs;\n if (\n startupTimeoutMs !== undefined &&\n (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs <= 0)\n ) {\n throw new Error(\"Invalid appServer.startupTimeoutMs. Expected a positive integer.\");\n }\n }\n\n if (this.backend === \"remote\") {\n if (!(\"url\" in options) || typeof options.url !== \"string\" || options.url.length === 0) {\n throw new Error(\"LettaAgentClient remote backend requires a non-empty url.\");\n }\n if (\n options.requestTimeoutMs !== undefined &&\n (!Number.isInteger(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)\n ) {\n throw new Error(\"Invalid requestTimeoutMs. Expected a positive integer.\");\n }\n }\n\n if (this.backend === \"cloud\") {\n validateCloudClientOptions(options as LettaCodeCloudClientOptions);\n }\n }\n\n /**\n * Create a new Letta Code agent with a default conversation.\n *\n * Environment/device selection is intentionally not part of the agent payload;\n * it belongs to the client/session execution context.\n */\n get repositories(): RepositoriesClient {\n this.assertOpen();\n return this.getRepositoriesClient();\n }\n\n /** Discover and resolve computers registered with this Letta Cloud account. */\n get computers(): ComputersClient {\n this.assertOpen();\n return this.getComputersClient();\n }\n\n async createAgent(options: CreateAgentOptions = {}): Promise<string> {\n this.assertOpen();\n if (hasCreateAgentEnvironment(options)) {\n throw new Error(\n \"createAgent() does not accept environment. Set a client default or pass environment to resumeSession()/createSession().\",\n );\n }\n\n validateCreateAgentOptions(options);\n\n // Resolve skill directory paths to inline skills up front so every\n // backend receives the same shape. Directory loading is Node-only; the\n // portable client resolves inline skills and rejects paths.\n if (options.skills !== undefined && options.skills.length > 0) {\n const nodeSupport = this.skillNodeSupport();\n const skills = await resolveSkillItems(\n options.skills,\n nodeSupport?.loadSkillDirectory,\n );\n options = { ...options, skills };\n }\n\n if (this.backend === \"remote\") {\n const session = new AppServerSession(this.appServerSessionOptions(), {\n kind: \"create-agent\",\n options,\n });\n const initMsg = await session.initialize();\n session.close();\n if (!initMsg.agentId) {\n throw new Error(\"App Server agent creation did not return an agent id.\");\n }\n return initMsg.agentId;\n }\n if (this.backend === \"cloud\") {\n return createCloudAgent(\n this.getCloudClient(),\n options,\n this.skillNodeSupport()?.pushSkillSupportFiles,\n );\n }\n return this.createLocalAgent(options);\n }\n\n /**\n * Create a new conversation/session.\n *\n * The app-server protocol requires an explicit agent id.\n */\n createSession(\n agentId: string,\n options: LettaCodeClientSessionOptions = {},\n ): LettaCodeSession {\n this.assertOpen();\n if (typeof agentId !== \"string\" || agentId.length === 0) {\n throw new Error(\"createSession() requires a non-empty agent id.\");\n }\n const sessionOptions = stripCloudExecutionOptions(options);\n validateCreateSessionOptions(sessionOptions);\n this.assertSessionBackend(\"createSession\", options);\n\n if (this.backend === \"remote\") {\n return new AppServerSession(this.appServerSessionOptions(), {\n kind: \"session\",\n agentId,\n newConversation: true,\n options,\n });\n }\n if (this.backend === \"cloud\") {\n return new CloudEnvironmentSession(\n this.cloudOptions(),\n {\n kind: \"session\",\n agentId,\n newConversation: true,\n options,\n },\n this.getCloudClient(),\n );\n }\n return this.createLocalSession(agentId, options);\n }\n\n /**\n * Resume an existing agent default conversation or a specific conversation.\n *\n * `options.environment` overrides the client's default execution target for\n * Letta Cloud sessions. Remote app-server URLs already select the runtime.\n */\n resumeSession(\n id: string,\n options: LettaCodeClientSessionOptions = {},\n ): LettaCodeSession {\n this.assertOpen();\n const sessionOptions = stripCloudExecutionOptions(options);\n validateCreateSessionOptions(sessionOptions);\n this.assertSessionBackend(\"resumeSession\", options);\n\n if (this.backend === \"remote\") {\n if (looksLikeConversationId(id)) {\n return new AppServerSession(this.appServerSessionOptions(), {\n kind: \"session\",\n conversationId: id,\n options,\n });\n }\n return new AppServerSession(this.appServerSessionOptions(), {\n kind: \"session\",\n agentId: id,\n defaultConversation: true,\n options,\n });\n }\n if (this.backend === \"cloud\") {\n if (looksLikeConversationId(id)) {\n return new CloudEnvironmentSession(\n this.cloudOptions(),\n {\n kind: \"session\",\n conversationId: id,\n options,\n },\n this.getCloudClient(),\n );\n }\n return new CloudEnvironmentSession(\n this.cloudOptions(),\n {\n kind: \"session\",\n agentId: id,\n defaultConversation: true,\n options,\n },\n this.getCloudClient(),\n );\n }\n return this.resumeLocalSession(id, options);\n }\n\n /**\n * One-shot convenience for scripts, smoke tests, and evals.\n * Applications should use a session when they need interactive turn state.\n */\n async prompt(\n message: SendMessage,\n agentId: string,\n options: LettaCodeClientSessionOptions = {},\n ): Promise<SDKResultMessage> {\n this.assertOpen();\n const session = this.createSession(agentId, options);\n\n try {\n return await (session as OneShotSession).sendAndWaitForResult(message);\n } finally {\n session.close();\n }\n }\n\n /** Run one prompt in a new or resumed ordinary conversation. */\n query(params: QueryParams): Query {\n this.assertOpen();\n validateQueryOptions(params.options);\n return createQuery((options) => this.createQuerySession(options), params);\n }\n\n private async createQuerySession(\n options: QueryOptions,\n ): Promise<LettaCodeSession> {\n this.assertOpen();\n validateQueryOptions(options);\n const sessionOptions = querySessionOptions(options);\n this.assertSessionBackend(\"query\", sessionOptions);\n if (options.disableMemoryGuard === true && this.backend === \"cloud\") {\n throw new Error(\n 'query() disableMemoryGuard is only supported with backend: \"local\" or \"remote\".',\n );\n }\n\n if (this.backend === \"remote\") {\n return new AppServerSession(this.appServerSessionOptions(), {\n kind: \"conversation\",\n ...(options.conversationId\n ? { conversationId: options.conversationId }\n : {\n createConversation: {\n model: options.model,\n system: options.system,\n ...(options.parentAgentId !== undefined\n ? { parentAgentId: options.parentAgentId }\n : {}),\n ...(options.name !== undefined ? { name: options.name } : {}),\n ...(options.isSubagent !== undefined\n ? { isSubagent: options.isSubagent }\n : {}),\n ...(options.modelSettings !== undefined\n ? { modelSettings: options.modelSettings }\n : {}),\n ...(options.contextWindowLimit !== undefined\n ? { contextWindowLimit: options.contextWindowLimit }\n : {}),\n },\n }),\n options: sessionOptions,\n ...(options.disableMemoryGuard === true\n ? { disableMemoryGuard: true }\n : {}),\n });\n }\n if (this.backend === \"cloud\") {\n const computer = options.computer ?? options.environment ?? this.computer;\n if (computer === undefined) {\n throw new Error(\n \"Cloud query() requires an explicit computer; managed sandboxes are agent-scoped.\",\n );\n }\n const conversation = options.conversationId\n ? { id: options.conversationId }\n : await this.getCloudClient().post<unknown>(\n \"/v1/conversations/ephemeral\",\n {\n body: {\n model: options.model,\n system: options.system,\n ...(options.parentAgentId !== undefined\n ? { parent_agent_id: options.parentAgentId }\n : {}),\n ...(options.name !== undefined ? { name: options.name } : {}),\n ...(options.isSubagent !== undefined\n ? { is_subagent: options.isSubagent }\n : {}),\n ...(options.modelSettings !== undefined\n ? { model_settings: options.modelSettings }\n : {}),\n ...(options.contextWindowLimit !== undefined\n ? { context_window_limit: options.contextWindowLimit }\n : {}),\n },\n },\n );\n if (\n !conversation ||\n typeof conversation !== \"object\" ||\n typeof (conversation as { id?: unknown }).id !== \"string\"\n ) {\n throw new Error(\n \"Cloud query conversation response did not include a conversation id.\",\n );\n }\n return new CloudEnvironmentSession(\n this.cloudOptions(),\n {\n kind: \"conversation\",\n conversationId: (conversation as { id: string }).id,\n options: sessionOptions,\n },\n this.getCloudClient(),\n );\n }\n return this.createLocalQuerySession(options, sessionOptions);\n }\n\n private assertSessionBackend(\n action: string,\n options: LettaCodeClientSessionOptions,\n ): void {\n if (options.computer !== undefined && options.environment !== undefined) {\n throw new Error(\n `${action}() cannot specify both computer and deprecated environment.`,\n );\n }\n if (\n options.filesystemConfinement !== undefined &&\n options.filesystemConfinement !== \"memory\"\n ) {\n throw new Error(\n `Invalid filesystemConfinement '${String(options.filesystemConfinement)}'. Valid value: memory.`,\n );\n }\n const effectiveComputer =\n options.computer ?? options.environment ?? this.computer;\n if (this.backend === \"local\") {\n if (effectiveComputer !== undefined) {\n const field = options.computer !== undefined ? \"computer\" : \"environment\";\n throw new Error(\n `${action}() ${field} overrides are only valid with backend: \"cloud\".`,\n );\n }\n if (options.sandbox !== undefined) {\n throw new Error(`${action}() sandbox options are only valid with backend: \"cloud\".`);\n }\n if (hasRepositoryResources(options)) {\n throw new Error(`${action}() repository resources are only valid with backend: \"cloud\".`);\n }\n if (options.filesystemConfinement !== undefined) {\n const localOptions = this.options as LettaCodeLocalClientOptions;\n if (localOptions.appServer?.url !== undefined) {\n throw new Error(\n `${action}() filesystemConfinement requires an SDK-owned local app-server process.`,\n );\n }\n }\n return;\n }\n\n if (this.backend === \"remote\") {\n if (options.filesystemConfinement !== undefined) {\n throw new Error(\n `${action}() filesystemConfinement requires an SDK-owned local app-server process.`,\n );\n }\n if (options.computer !== undefined || options.environment !== undefined) {\n const field = options.computer !== undefined ? \"computer\" : \"environment\";\n throw new Error(\n `${action}() ${field} overrides are only valid with backend: \"cloud\"; remote url selects the app-server runtime.`,\n );\n }\n if (options.sandbox !== undefined) {\n throw new Error(`${action}() sandbox options are only valid with backend: \"cloud\"; remote url selects the app-server runtime.`);\n }\n if (hasRepositoryResources(options)) {\n throw new Error(`${action}() repository resources are only valid with backend: \"cloud\".`);\n }\n return;\n }\n if (this.backend === \"cloud\") {\n if (options.filesystemConfinement !== undefined) {\n throw new Error(\n `${action}() filesystemConfinement is only supported with backend: \"local\".`,\n );\n }\n const cloudOptions = this.cloudOptions();\n if (this.computer !== undefined && options.sandbox !== undefined) {\n const field = cloudOptions.computer !== undefined\n ? \"computer\"\n : \"environment\";\n throw new Error(`Letta Cloud ${action}() cannot specify sandbox options when the client has a default ${field}.`);\n }\n if (\n cloudOptions.sandbox !== undefined &&\n (options.computer !== undefined || options.environment !== undefined)\n ) {\n const field = options.computer !== undefined\n ? \"a computer\"\n : \"an environment\";\n throw new Error(`Letta Cloud ${action}() cannot specify ${field} when the client has default sandbox options.`);\n }\n assertCloudSessionOptionsSupported(action, options);\n return;\n }\n throw new Error(\n `LettaAgentClient backend '${this.backend}' is not implemented yet. ${action} currently supports backend 'local' only.`,\n );\n }\n\n protected createLocalAgent(_options: CreateAgentOptions): Promise<string> {\n throw this.localBackendUnavailableError();\n }\n\n /**\n * Node-only skill seeding support (directory loading, memory-repo git\n * push). The Node client overrides this; the portable client returns\n * undefined, which limits skills to inline SKILL.md-only objects.\n */\n protected skillNodeSupport(): SkillNodeSupport | undefined {\n return undefined;\n }\n\n protected createLocalSession(\n _agentId: string,\n _options: LettaCodeClientSessionOptions,\n ): LettaCodeSession {\n throw this.localBackendUnavailableError();\n }\n\n protected createLocalQuerySession(\n _queryOptions: QueryOptions,\n _sessionOptions: LettaCodeClientSessionOptions,\n ): LettaCodeSession {\n throw this.localBackendUnavailableError();\n }\n\n protected resumeLocalSession(\n _id: string,\n _options: LettaCodeClientSessionOptions,\n ): LettaCodeSession {\n throw this.localBackendUnavailableError();\n }\n\n protected localBackendUnavailableError(): Error {\n return new Error(\n 'The portable \"@letta-ai/letta-agent-sdk/client\" entry point supports backend: \"remote\" and backend: \"cloud\" only. Import from \"@letta-ai/letta-agent-sdk\" for local execution.',\n );\n }\n\n protected createLocalManagementTransport(): ManagementTransport {\n throw this.localBackendUnavailableError();\n }\n\n private remoteOptions(): LettaCodeRemoteClientOptions {\n if (this.backend !== \"remote\") {\n throw new Error(\"Remote options requested for non-remote backend.\");\n }\n return this.options as LettaCodeRemoteClientOptions;\n }\n\n private appServerSessionOptions(): AppServerSessionOptions {\n return this.remoteOptions();\n }\n\n private getComputersClient(): ComputersClientImpl {\n if (this.backend !== \"cloud\") {\n throw new Error('client.computers is only available with backend: \"cloud\".');\n }\n this.computersClient ??= new ComputersClientImpl(\n this.getCloudClient(),\n () => this.assertOpen(),\n );\n return this.computersClient;\n }\n\n private getRepositoriesClient(): RepositoriesClient {\n if (this.backend !== \"cloud\") {\n throw new Error('client.repositories is only available with backend: \"cloud\".');\n }\n this.repositoriesClient ??= new RepositoriesClient(\n this.cloudOptions(),\n this.getCloudClient(),\n () => this.assertOpen(),\n );\n return this.repositoriesClient;\n }\n\n private getAgentRepositoriesClient(): AgentRepositoriesClient {\n if (this.backend !== \"cloud\") {\n throw new Error(\n 'client.agents.repositories is only available with backend: \"cloud\".',\n );\n }\n this.agentRepositoriesClient ??= createAgentRepositoriesClient(\n () => this.getCloudManagementTransport(),\n );\n return this.agentRepositoriesClient;\n }\n\n private getCloudManagementTransport(): CloudManagementTransport {\n const transport = this.getManagementTransport();\n if (!(transport instanceof CloudManagementTransport)) {\n throw new Error(\"Cloud management requested for non-cloud backend.\");\n }\n return transport;\n }\n\n private getCloudClient(): Letta {\n if (this.backend !== \"cloud\") {\n throw new Error(\"Letta client requested for non-cloud backend.\");\n }\n this.cloudClient ??= createCloudClient(this.cloudOptions());\n return this.cloudClient;\n }\n\n private getManagementTransport(): ManagementTransport {\n this.assertOpen();\n if (this.managementTransport) return this.managementTransport;\n if (this.backend === \"remote\") {\n this.managementTransport = new AppServerManagementTransport(\n this.remoteOptions(),\n );\n } else if (this.backend === \"cloud\") {\n this.managementTransport = new CloudManagementTransport(\n this.getCloudClient(),\n );\n } else {\n this.managementTransport = this.createLocalManagementTransport();\n }\n return this.managementTransport;\n }\n\n /**\n * Release resources owned directly by this client, including its pooled\n * management connection and any local App Server started for that pool.\n * Sessions have independent lifecycles and must be closed separately.\n */\n close(): Promise<void> {\n if (this.closePromise) return this.closePromise;\n this.closed = true;\n const transport = this.managementTransport;\n this.managementTransport = null;\n const close = transport?.close() ?? Promise.resolve();\n this.closePromise = close;\n return close;\n }\n\n /** Async-disposal alias for close(). */\n async [Symbol.asyncDispose](): Promise<void> {\n await this.close();\n }\n\n private assertOpen(): void {\n if (this.closed) throw new Error(\"LettaAgentClient is closed\");\n }\n\n private cloudOptions(): LettaCodeCloudClientOptions {\n if (this.backend !== \"cloud\") {\n throw new Error('Letta Cloud options requested for non-\"cloud\" backend.');\n }\n return this.options as LettaCodeCloudClientOptions;\n }\n}\n",
|
|
170
|
+
"import type Letta from \"@letta-ai/letta-client\";\nimport { RepositoriesClient } from \"./repositories.js\";\nimport { resolveSkillItems, type SkillNodeSupport } from \"./skill-loading.js\";\nimport { createAgentRepositoriesClient } from \"./agent-repositories.js\";\nimport { AppServerManagementTransport } from \"./app-server-management.js\";\nimport {\n AppServerSession,\n type AppServerSessionOptions,\n} from \"./app-server-session.js\";\nimport { CloudManagementTransport } from \"./cloud-management.js\";\nimport { createCloudClient } from \"./cloud-client.js\";\nimport {\n ComputersClientImpl,\n type ComputerSelector,\n type ComputersClient,\n} from \"./computers.js\";\nimport {\n CloudEnvironmentSession,\n assertCloudSessionOptionsSupported,\n createCloudAgent,\n validateCloudClientOptions,\n} from \"./cloud-session.js\";\nimport {\n createAgentsClient,\n createConversationsClient,\n createModelsClient,\n type ManagementTransport,\n} from \"./management.js\";\nimport type {\n AgentRepositoriesClient,\n AgentsClient,\n ConversationsClient,\n ModelsClient,\n} from \"./management-types.js\";\nimport { createQuery } from \"./query.js\";\nimport type {\n Query,\n QueryOptions,\n QueryParams,\n} from \"./query-types.js\";\nimport type {\n CreateAgentOptions,\n CreateSessionOptions,\n LettaCodeBackend,\n LettaCodeClientOptions,\n LettaCodeClientSessionOptions,\n LettaCodeEnvironment,\n LettaCodeRemoteClientOptions,\n LettaCodeLocalClientOptions,\n LettaCodeCloudClientOptions,\n LettaCodeSession,\n SDKResultMessage,\n SendMessage,\n} from \"./types.js\";\nimport {\n validateCreateAgentOptions,\n validateCreateSessionOptions,\n} from \"./validation.js\";\n\nconst VALID_BACKENDS = new Set<LettaCodeBackend>([\n \"local\",\n \"remote\",\n \"cloud\",\n]);\n\nfunction isLettaCodeBackend(value: string): value is LettaCodeBackend {\n return VALID_BACKENDS.has(value as LettaCodeBackend);\n}\n\nfunction getOptionsComputer(\n options: LettaCodeClientOptions,\n): ComputerSelector | undefined {\n if (!(\"computer\" in options) && !(\"environment\" in options)) return undefined;\n if (options.computer !== undefined && options.environment !== undefined) {\n throw new Error('Specify either \"computer\" or deprecated \"environment\", not both.');\n }\n return options.computer ?? options.environment;\n}\n\nfunction stripCloudExecutionOptions(\n options: LettaCodeClientSessionOptions,\n): CreateSessionOptions {\n const sessionOptions = { ...options };\n delete sessionOptions.computer;\n delete sessionOptions.environment;\n delete sessionOptions.sandbox;\n delete sessionOptions.filesystemConfinement;\n return sessionOptions;\n}\n\nfunction hasRepositoryResources(options: LettaCodeClientSessionOptions): boolean {\n return options.resources !== undefined && options.resources.length > 0;\n}\n\nfunction hasCreateAgentEnvironment(options: CreateAgentOptions): boolean {\n return \"environment\" in (options as Record<string, unknown>);\n}\n\nfunction looksLikeConversationId(id: string): boolean {\n return id.startsWith(\"conv-\") || id.startsWith(\"local-conv-\");\n}\n\nfunction querySessionOptions(\n options: QueryOptions,\n): LettaCodeClientSessionOptions {\n const {\n system: _system,\n parentAgentId: _parentAgentId,\n name: _name,\n isSubagent: _isSubagent,\n modelSettings: _modelSettings,\n contextWindowLimit: _contextWindowLimit,\n conversationId: _conversationId,\n disableMemoryGuard: _disableMemoryGuard,\n ...sessionOptions\n } = options;\n return sessionOptions;\n}\n\nfunction validateQueryOptions(options: QueryOptions): void {\n if (\n options.conversationId !== undefined &&\n (typeof options.conversationId !== \"string\" || !options.conversationId.trim())\n ) {\n throw new Error(\"query() conversationId must be a non-empty string.\");\n }\n if (typeof options.model !== \"string\" || options.model.length === 0) {\n throw new Error(\"query() requires a non-empty model.\");\n }\n if (typeof options.system !== \"string\") {\n throw new Error(\"query() requires a system prompt.\");\n }\n validateCreateSessionOptions(querySessionOptions(options));\n}\n\ntype OneShotSession = LettaCodeSession & {\n sendAndWaitForResult(message: SendMessage): Promise<SDKResultMessage>;\n};\n\n/**\n * Top-level Letta Agent SDK client.\n *\n * `backend` selects how the SDK reaches or runs the Letta Code harness.\n * `local` spawns an SDK-owned Letta Code app-server and speaks the websocket\n * protocol.\n * `remote` connects to a user-managed Letta Code app-server websocket endpoint.\n * `cloud` uses agents hosted on Letta Cloud, with an explicit remote\n * environment or SDK-managed sandbox.\n */\nexport class LettaAgentClientBase implements AsyncDisposable {\n readonly backend: LettaCodeBackend;\n readonly computer: ComputerSelector | undefined;\n /** @deprecated Use `computer`. */\n readonly environment: LettaCodeEnvironment | undefined;\n readonly agents: AgentsClient;\n readonly conversations: ConversationsClient;\n readonly models: ModelsClient;\n protected readonly options: LettaCodeClientOptions;\n private repositoriesClient: RepositoriesClient | null = null;\n private computersClient: ComputersClientImpl | null = null;\n private agentRepositoriesClient: AgentRepositoriesClient | null = null;\n private cloudClient: Letta | null = null;\n private managementTransport: ManagementTransport | null = null;\n private closed = false;\n private closePromise: Promise<void> | null = null;\n\n constructor(options: LettaCodeClientOptions = {}) {\n const backend = options.backend ?? \"local\";\n if (!isLettaCodeBackend(backend)) {\n throw new Error(\n `Invalid Letta Code backend '${String(backend)}'. Valid values: local, remote, cloud.`,\n );\n }\n\n this.backend = backend;\n this.computer = getOptionsComputer(options);\n this.environment = this.computer;\n this.options = options;\n this.agents = createAgentsClient(\n () => this.getManagementTransport(),\n () => this.getAgentRepositoriesClient(),\n );\n this.conversations = createConversationsClient(\n () => this.getManagementTransport(),\n );\n this.models = createModelsClient(() => this.getManagementTransport());\n\n if (this.backend === \"local\" && this.computer !== undefined) {\n const field = \"computer\" in options && options.computer !== undefined\n ? \"computer\"\n : \"environment\";\n throw new Error(\n `LettaAgentClient ${field} is only valid with backend: \"cloud\".`,\n );\n }\n if (this.backend === \"remote\" && this.computer !== undefined) {\n const field = \"computer\" in options && options.computer !== undefined\n ? \"computer\"\n : \"environment\";\n throw new Error(\n `LettaAgentClient ${field} is only valid with backend: \"cloud\"; remote url selects the app-server runtime.`,\n );\n }\n if (this.backend !== \"cloud\" && (options as { sandbox?: unknown }).sandbox !== undefined) {\n throw new Error('LettaAgentClient sandbox options are only valid with backend: \"cloud\".');\n }\n\n if (this.backend === \"local\") {\n const localOptions = options as LettaCodeLocalClientOptions;\n if (\"transport\" in localOptions) {\n throw new Error(\n 'Local transport selection has been removed. The local backend always uses the app-server protocol.',\n );\n }\n const requestTimeoutMs = localOptions.appServer?.requestTimeoutMs;\n if (\n requestTimeoutMs !== undefined &&\n (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0)\n ) {\n throw new Error(\"Invalid appServer.requestTimeoutMs. Expected a positive integer.\");\n }\n const startupTimeoutMs = localOptions.appServer?.startupTimeoutMs;\n if (\n startupTimeoutMs !== undefined &&\n (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs <= 0)\n ) {\n throw new Error(\"Invalid appServer.startupTimeoutMs. Expected a positive integer.\");\n }\n }\n\n if (this.backend === \"remote\") {\n if (!(\"url\" in options) || typeof options.url !== \"string\" || options.url.length === 0) {\n throw new Error(\"LettaAgentClient remote backend requires a non-empty url.\");\n }\n if (\n options.requestTimeoutMs !== undefined &&\n (!Number.isInteger(options.requestTimeoutMs) || options.requestTimeoutMs <= 0)\n ) {\n throw new Error(\"Invalid requestTimeoutMs. Expected a positive integer.\");\n }\n }\n\n if (this.backend === \"cloud\") {\n validateCloudClientOptions(options as LettaCodeCloudClientOptions);\n }\n }\n\n /**\n * Create a new Letta Code agent with a default conversation.\n *\n * Environment/device selection is intentionally not part of the agent payload;\n * it belongs to the client/session execution context.\n */\n get repositories(): RepositoriesClient {\n this.assertOpen();\n return this.getRepositoriesClient();\n }\n\n /** Discover and resolve computers registered with this Letta Cloud account. */\n get computers(): ComputersClient {\n this.assertOpen();\n return this.getComputersClient();\n }\n\n async createAgent(options: CreateAgentOptions = {}): Promise<string> {\n this.assertOpen();\n if (hasCreateAgentEnvironment(options)) {\n throw new Error(\n \"createAgent() does not accept environment. Set a client default or pass environment to resumeSession()/createSession().\",\n );\n }\n\n validateCreateAgentOptions(options);\n\n // Resolve skill directory paths to inline skills up front so every\n // backend receives the same shape. Directory loading is Node-only; the\n // portable client resolves inline skills and rejects paths.\n if (options.skills !== undefined && options.skills.length > 0) {\n const nodeSupport = this.skillNodeSupport();\n const skills = await resolveSkillItems(\n options.skills,\n nodeSupport?.loadSkillDirectory,\n );\n options = { ...options, skills };\n }\n\n if (this.backend === \"remote\") {\n const session = new AppServerSession(this.appServerSessionOptions(), {\n kind: \"create-agent\",\n options,\n });\n const initMsg = await session.initialize();\n session.close();\n if (!initMsg.agentId) {\n throw new Error(\"App Server agent creation did not return an agent id.\");\n }\n return initMsg.agentId;\n }\n if (this.backend === \"cloud\") {\n return createCloudAgent(\n this.getCloudClient(),\n options,\n this.skillNodeSupport()?.pushSkillSupportFiles,\n );\n }\n return this.createLocalAgent(options);\n }\n\n /**\n * Create a new conversation/session.\n *\n * The app-server protocol requires an explicit agent id.\n */\n createSession(\n agentId: string,\n options: LettaCodeClientSessionOptions = {},\n ): LettaCodeSession {\n this.assertOpen();\n if (typeof agentId !== \"string\" || agentId.length === 0) {\n throw new Error(\"createSession() requires a non-empty agent id.\");\n }\n const sessionOptions = stripCloudExecutionOptions(options);\n validateCreateSessionOptions(sessionOptions);\n this.assertSessionBackend(\"createSession\", options);\n\n if (this.backend === \"remote\") {\n return new AppServerSession(this.appServerSessionOptions(), {\n kind: \"session\",\n agentId,\n newConversation: true,\n options,\n });\n }\n if (this.backend === \"cloud\") {\n return new CloudEnvironmentSession(\n this.cloudOptions(),\n {\n kind: \"session\",\n agentId,\n newConversation: true,\n options,\n },\n this.getCloudClient(),\n );\n }\n return this.createLocalSession(agentId, options);\n }\n\n /**\n * Resume an existing agent default conversation or a specific conversation.\n *\n * `options.environment` overrides the client's default execution target for\n * Letta Cloud sessions. Remote app-server URLs already select the runtime.\n */\n resumeSession(\n id: string,\n options: LettaCodeClientSessionOptions = {},\n ): LettaCodeSession {\n this.assertOpen();\n const sessionOptions = stripCloudExecutionOptions(options);\n validateCreateSessionOptions(sessionOptions);\n this.assertSessionBackend(\"resumeSession\", options);\n\n if (this.backend === \"remote\") {\n if (looksLikeConversationId(id)) {\n return new AppServerSession(this.appServerSessionOptions(), {\n kind: \"session\",\n conversationId: id,\n options,\n });\n }\n return new AppServerSession(this.appServerSessionOptions(), {\n kind: \"session\",\n agentId: id,\n defaultConversation: true,\n options,\n });\n }\n if (this.backend === \"cloud\") {\n if (looksLikeConversationId(id)) {\n return new CloudEnvironmentSession(\n this.cloudOptions(),\n {\n kind: \"session\",\n conversationId: id,\n options,\n },\n this.getCloudClient(),\n );\n }\n return new CloudEnvironmentSession(\n this.cloudOptions(),\n {\n kind: \"session\",\n agentId: id,\n defaultConversation: true,\n options,\n },\n this.getCloudClient(),\n );\n }\n return this.resumeLocalSession(id, options);\n }\n\n /**\n * One-shot convenience for scripts, smoke tests, and evals.\n * Applications should use a session when they need interactive turn state.\n */\n async prompt(\n message: SendMessage,\n agentId: string,\n options: LettaCodeClientSessionOptions = {},\n ): Promise<SDKResultMessage> {\n this.assertOpen();\n const session = this.createSession(agentId, options);\n\n try {\n return await (session as OneShotSession).sendAndWaitForResult(message);\n } finally {\n session.close();\n }\n }\n\n /** Run one prompt in a new or resumed ordinary conversation. */\n query(params: QueryParams): Query {\n this.assertOpen();\n validateQueryOptions(params.options);\n return createQuery((options) => this.createQuerySession(options), params);\n }\n\n private async createQuerySession(\n options: QueryOptions,\n ): Promise<LettaCodeSession> {\n this.assertOpen();\n validateQueryOptions(options);\n const sessionOptions = querySessionOptions(options);\n this.assertSessionBackend(\"query\", sessionOptions);\n if (options.disableMemoryGuard === true && this.backend === \"cloud\") {\n throw new Error(\n 'query() disableMemoryGuard is only supported with backend: \"local\" or \"remote\".',\n );\n }\n\n if (this.backend === \"remote\") {\n return new AppServerSession(this.appServerSessionOptions(), {\n kind: \"conversation\",\n ...(options.conversationId\n ? { conversationId: options.conversationId }\n : {\n createConversation: {\n model: options.model,\n system: options.system,\n ...(options.parentAgentId !== undefined\n ? { parentAgentId: options.parentAgentId }\n : {}),\n ...(options.name !== undefined ? { name: options.name } : {}),\n ...(options.isSubagent !== undefined\n ? { isSubagent: options.isSubagent }\n : {}),\n ...(options.modelSettings !== undefined\n ? { modelSettings: options.modelSettings }\n : {}),\n ...(options.contextWindowLimit !== undefined\n ? { contextWindowLimit: options.contextWindowLimit }\n : {}),\n },\n }),\n options: sessionOptions,\n ...(options.disableMemoryGuard === true\n ? { disableMemoryGuard: true }\n : {}),\n });\n }\n if (this.backend === \"cloud\") {\n const computer = options.computer ?? options.environment ?? this.computer;\n if (computer === undefined) {\n throw new Error(\n \"Cloud query() requires an explicit computer; managed sandboxes are agent-scoped.\",\n );\n }\n const conversation = options.conversationId\n ? { id: options.conversationId }\n : await this.getCloudClient().post<unknown>(\n \"/v1/conversations/ephemeral\",\n {\n body: {\n model: options.model,\n system: options.system,\n ...(options.parentAgentId !== undefined\n ? { parent_agent_id: options.parentAgentId }\n : {}),\n ...(options.name !== undefined ? { name: options.name } : {}),\n ...(options.isSubagent !== undefined\n ? { is_subagent: options.isSubagent }\n : {}),\n ...(options.modelSettings !== undefined\n ? { model_settings: options.modelSettings }\n : {}),\n ...(options.contextWindowLimit !== undefined\n ? { context_window_limit: options.contextWindowLimit }\n : {}),\n },\n },\n );\n if (\n !conversation ||\n typeof conversation !== \"object\" ||\n typeof (conversation as { id?: unknown }).id !== \"string\"\n ) {\n throw new Error(\n \"Cloud query conversation response did not include a conversation id.\",\n );\n }\n return new CloudEnvironmentSession(\n this.cloudOptions(),\n {\n kind: \"conversation\",\n conversationId: (conversation as { id: string }).id,\n options: sessionOptions,\n },\n this.getCloudClient(),\n );\n }\n return this.createLocalQuerySession(options, sessionOptions);\n }\n\n private assertSessionBackend(\n action: string,\n options: LettaCodeClientSessionOptions,\n ): void {\n if (options.computer !== undefined && options.environment !== undefined) {\n throw new Error(\n `${action}() cannot specify both computer and deprecated environment.`,\n );\n }\n if (\n options.filesystemConfinement !== undefined &&\n options.filesystemConfinement !== \"memory\"\n ) {\n throw new Error(\n `Invalid filesystemConfinement '${String(options.filesystemConfinement)}'. Valid value: memory.`,\n );\n }\n const effectiveComputer =\n options.computer ?? options.environment ?? this.computer;\n if (this.backend === \"local\") {\n if (effectiveComputer !== undefined) {\n const field = options.computer !== undefined ? \"computer\" : \"environment\";\n throw new Error(\n `${action}() ${field} overrides are only valid with backend: \"cloud\".`,\n );\n }\n if (options.sandbox !== undefined) {\n throw new Error(`${action}() sandbox options are only valid with backend: \"cloud\".`);\n }\n if (hasRepositoryResources(options)) {\n throw new Error(`${action}() repository resources are only valid with backend: \"cloud\".`);\n }\n if (options.filesystemConfinement !== undefined) {\n const localOptions = this.options as LettaCodeLocalClientOptions;\n if (localOptions.appServer?.url !== undefined) {\n throw new Error(\n `${action}() filesystemConfinement requires an SDK-owned local app-server process.`,\n );\n }\n }\n return;\n }\n\n if (this.backend === \"remote\") {\n if (options.filesystemConfinement !== undefined) {\n throw new Error(\n `${action}() filesystemConfinement requires an SDK-owned local app-server process.`,\n );\n }\n if (options.computer !== undefined || options.environment !== undefined) {\n const field = options.computer !== undefined ? \"computer\" : \"environment\";\n throw new Error(\n `${action}() ${field} overrides are only valid with backend: \"cloud\"; remote url selects the app-server runtime.`,\n );\n }\n if (options.sandbox !== undefined) {\n throw new Error(`${action}() sandbox options are only valid with backend: \"cloud\"; remote url selects the app-server runtime.`);\n }\n if (hasRepositoryResources(options)) {\n throw new Error(`${action}() repository resources are only valid with backend: \"cloud\".`);\n }\n return;\n }\n if (this.backend === \"cloud\") {\n if (options.filesystemConfinement !== undefined) {\n throw new Error(\n `${action}() filesystemConfinement is only supported with backend: \"local\".`,\n );\n }\n const cloudOptions = this.cloudOptions();\n if (this.computer !== undefined && options.sandbox !== undefined) {\n const field = cloudOptions.computer !== undefined\n ? \"computer\"\n : \"environment\";\n throw new Error(`Letta Cloud ${action}() cannot specify sandbox options when the client has a default ${field}.`);\n }\n if (\n cloudOptions.sandbox !== undefined &&\n (options.computer !== undefined || options.environment !== undefined)\n ) {\n const field = options.computer !== undefined\n ? \"a computer\"\n : \"an environment\";\n throw new Error(`Letta Cloud ${action}() cannot specify ${field} when the client has default sandbox options.`);\n }\n assertCloudSessionOptionsSupported(action, options);\n return;\n }\n throw new Error(\n `LettaAgentClient backend '${this.backend}' is not implemented yet. ${action} currently supports backend 'local' only.`,\n );\n }\n\n protected createLocalAgent(_options: CreateAgentOptions): Promise<string> {\n throw this.localBackendUnavailableError();\n }\n\n /**\n * Node-only skill seeding support (directory loading, memory-repo git\n * push). The Node client overrides this; the portable client returns\n * undefined, which limits skills to inline SKILL.md-only objects.\n */\n protected skillNodeSupport(): SkillNodeSupport | undefined {\n return undefined;\n }\n\n protected createLocalSession(\n _agentId: string,\n _options: LettaCodeClientSessionOptions,\n ): LettaCodeSession {\n throw this.localBackendUnavailableError();\n }\n\n protected createLocalQuerySession(\n _queryOptions: QueryOptions,\n _sessionOptions: LettaCodeClientSessionOptions,\n ): LettaCodeSession {\n throw this.localBackendUnavailableError();\n }\n\n protected resumeLocalSession(\n _id: string,\n _options: LettaCodeClientSessionOptions,\n ): LettaCodeSession {\n throw this.localBackendUnavailableError();\n }\n\n protected localBackendUnavailableError(): Error {\n return new Error(\n 'The portable \"@letta-ai/letta-agent-sdk/client\" entry point supports backend: \"remote\" and backend: \"cloud\" only. Import from \"@letta-ai/letta-agent-sdk\" for local execution.',\n );\n }\n\n protected createLocalManagementTransport(): ManagementTransport {\n throw this.localBackendUnavailableError();\n }\n\n private remoteOptions(): LettaCodeRemoteClientOptions {\n if (this.backend !== \"remote\") {\n throw new Error(\"Remote options requested for non-remote backend.\");\n }\n return this.options as LettaCodeRemoteClientOptions;\n }\n\n private appServerSessionOptions(): AppServerSessionOptions {\n return this.remoteOptions();\n }\n\n private getComputersClient(): ComputersClientImpl {\n if (this.backend !== \"cloud\") {\n throw new Error('client.computers is only available with backend: \"cloud\".');\n }\n this.computersClient ??= new ComputersClientImpl(\n this.getCloudClient(),\n () => this.assertOpen(),\n );\n return this.computersClient;\n }\n\n private getRepositoriesClient(): RepositoriesClient {\n if (this.backend !== \"cloud\") {\n throw new Error('client.repositories is only available with backend: \"cloud\".');\n }\n this.repositoriesClient ??= new RepositoriesClient(\n this.cloudOptions(),\n this.getCloudClient(),\n () => this.assertOpen(),\n );\n return this.repositoriesClient;\n }\n\n private getAgentRepositoriesClient(): AgentRepositoriesClient {\n if (this.backend !== \"cloud\") {\n throw new Error(\n 'client.agents.repositories is only available with backend: \"cloud\".',\n );\n }\n this.agentRepositoriesClient ??= createAgentRepositoriesClient(\n () => this.getCloudManagementTransport(),\n );\n return this.agentRepositoriesClient;\n }\n\n private getCloudManagementTransport(): CloudManagementTransport {\n const transport = this.getManagementTransport();\n if (!(transport instanceof CloudManagementTransport)) {\n throw new Error(\"Cloud management requested for non-cloud backend.\");\n }\n return transport;\n }\n\n private getCloudClient(): Letta {\n if (this.backend !== \"cloud\") {\n throw new Error(\"Letta client requested for non-cloud backend.\");\n }\n this.cloudClient ??= createCloudClient(this.cloudOptions());\n return this.cloudClient;\n }\n\n private getManagementTransport(): ManagementTransport {\n this.assertOpen();\n if (this.managementTransport) return this.managementTransport;\n if (this.backend === \"remote\") {\n this.managementTransport = new AppServerManagementTransport(\n this.remoteOptions(),\n );\n } else if (this.backend === \"cloud\") {\n this.managementTransport = new CloudManagementTransport(\n this.getCloudClient(),\n );\n } else {\n this.managementTransport = this.createLocalManagementTransport();\n }\n return this.managementTransport;\n }\n\n /**\n * Release resources owned directly by this client, including its pooled\n * management connection. The Node client also stops its shared local App\n * Server; dedicated and externally hosted sessions remain independently owned.\n */\n close(): Promise<void> {\n if (this.closePromise) return this.closePromise;\n this.closed = true;\n const transport = this.managementTransport;\n this.managementTransport = null;\n const close = transport?.close() ?? Promise.resolve();\n this.closePromise = close;\n return close;\n }\n\n /** Async-disposal alias for close(). */\n async [Symbol.asyncDispose](): Promise<void> {\n await this.close();\n }\n\n private assertOpen(): void {\n if (this.closed) throw new Error(\"LettaAgentClient is closed\");\n }\n\n private cloudOptions(): LettaCodeCloudClientOptions {\n if (this.backend !== \"cloud\") {\n throw new Error('Letta Cloud options requested for non-\"cloud\" backend.');\n }\n return this.options as LettaCodeCloudClientOptions;\n }\n}\n",
|
|
171
171
|
"import type { SDKStreamEventPayload } from \"./types.js\";\n\nexport type StreamTextKind = \"assistant\" | \"reasoning\";\n\nexport interface StreamTextDelta {\n kind: StreamTextKind;\n text: string;\n}\n\nfunction extractTextFromContent(content: unknown): string | null {\n if (typeof content === \"string\") {\n return content;\n }\n\n if (Array.isArray(content)) {\n const pieces: string[] = [];\n for (const part of content) {\n if (typeof part === \"string\") {\n pieces.push(part);\n continue;\n }\n if (!part || typeof part !== \"object\") {\n continue;\n }\n const rec = part as Record<string, unknown>;\n if (typeof rec.text === \"string\") {\n pieces.push(rec.text);\n }\n }\n const joined = pieces.join(\"\");\n return joined.length > 0 ? joined : null;\n }\n\n if (content && typeof content === \"object\") {\n const rec = content as Record<string, unknown>;\n if (typeof rec.text === \"string\") {\n return rec.text;\n }\n }\n\n return null;\n}\n\n/**\n * Extract appendable assistant/reasoning text from a stream_event payload.\n *\n * Supports both shapes currently emitted by headless mode:\n * 1) content_block style: { type, delta: { text|reasoning } }\n * 2) message chunk style: { message_type: \"assistant_message\"|\"reasoning_message\", ... }\n */\nexport function extractStreamTextDelta(event: SDKStreamEventPayload): StreamTextDelta | null {\n if (!event || typeof event !== \"object\") {\n return null;\n }\n\n const rec = event as Record<string, unknown>;\n\n const maybeDelta = rec.delta;\n if (maybeDelta && typeof maybeDelta === \"object\") {\n const delta = maybeDelta as Record<string, unknown>;\n\n if (typeof delta.reasoning === \"string\" && delta.reasoning.length > 0) {\n return { kind: \"reasoning\", text: delta.reasoning };\n }\n\n if (typeof delta.text === \"string\" && delta.text.length > 0) {\n return { kind: \"assistant\", text: delta.text };\n }\n }\n\n const messageType = rec.message_type;\n if (messageType === \"reasoning_message\") {\n const reasoningText =\n typeof rec.reasoning === \"string\" ? rec.reasoning : extractTextFromContent(rec.content);\n if (reasoningText && reasoningText.length > 0) {\n return { kind: \"reasoning\", text: reasoningText };\n }\n }\n\n if (messageType === \"assistant_message\") {\n const assistantText = extractTextFromContent(rec.content);\n if (assistantText && assistantText.length > 0) {\n return { kind: \"assistant\", text: assistantText };\n }\n }\n\n return null;\n}\n",
|
|
172
172
|
"/**\n * Transcript accumulator\n *\n * Turns an `SDKMessage` stream into stable, render-ready rows so consumers stop\n * hand-rolling stream reconciliation. It owns the four reconciliation rules the\n * wire protocol requires:\n *\n * 1. Typed-by-family accumulation. Text slices are keyed on\n * `message family + otid`, falling back to `uuid` *within the same family*.\n * A bare `otid`/`uuid` key would collapse an assistant slice into a\n * reasoning slice whenever a provider reuses an identifier across kinds.\n * 2. Per-`runId` `seqId` replay suppression. Each run keeps its own high-water\n * mark, so a resumed stream that replays positions is dropped while a new\n * run starts from a clean threshold.\n * 3. `toolCallId`-keyed merging. Tool argument fragments and the eventual tool\n * result merge into one row keyed on the payload identity (`toolCallId`),\n * while the envelope identities (the `uuid` of the `tool_call` message and\n * of the `tool_result` message) stay separately visible.\n * 4. `rebase()` for mid-run backfill. A history page is merged in place with\n * replace semantics, reordered ahead of live-only rows, and raises the\n * replay thresholds it proves.\n *\n * The accumulator is pure and portable: no I/O, no timers, no Node built-ins,\n * so it is exported from both the package root and `/client`.\n */\n\nimport type { Message as LettaMessage } from \"@letta-ai/letta-client/resources/agents/messages\";\nimport {\n extractTextFromContent,\n firstToolCall,\n firstToolReturn,\n} from \"./remote-session-protocol.js\";\nimport { extractStreamTextDelta } from \"./stream-events.js\";\nimport type { SDKMessage, SDKStreamEventMessage } from \"./types.js\";\n\n// ═══════════════════════════════════════════════════════════════\n// PUBLIC TYPES\n// ═══════════════════════════════════════════════════════════════\n\n/** Message families the accumulator projects into rows. */\nexport type TranscriptRowKind = \"user\" | \"assistant\" | \"reasoning\" | \"tool_call\";\n\n/** Text families. Rows in different families never share a key. */\nexport type TranscriptTextKind = \"user\" | \"assistant\" | \"reasoning\";\n\nexport interface TranscriptRowIdentity {\n /**\n * Stable render key. Namespaced by message family, so a provider that reuses\n * an `otid` or a message id across kinds still produces separate rows.\n */\n key: string;\n /** Envelope id of the message that opened this row, when known. */\n uuid?: string;\n /** Lineage key for this typed slice, when the stream supplied one. */\n otid?: string;\n /** Run that most recently contributed to this row. */\n runId?: string;\n /** Highest replay cursor observed for this row. */\n seqId?: number;\n}\n\nexport interface TranscriptTextRow extends TranscriptRowIdentity {\n kind: TranscriptTextKind;\n /** Accumulated text for this slice. */\n text: string;\n}\n\nexport interface TranscriptToolResult {\n content: string;\n isError: boolean;\n /**\n * Envelope id of the `tool_result` message. Deliberately distinct from the\n * row's `uuid`, which identifies the `tool_call` envelope.\n */\n uuid?: string;\n}\n\n/**\n * Lifecycle of a tool row.\n *\n * - `streaming`: argument fragments are still arriving and have not parsed.\n * - `ready`: arguments parsed; the result has not arrived.\n * - `complete`: a tool result merged into the row.\n */\nexport type TranscriptToolCallStatus = \"streaming\" | \"ready\" | \"complete\";\n\nexport interface TranscriptToolCallRow extends TranscriptRowIdentity {\n kind: \"tool_call\";\n /** Payload identity. This is what the row is keyed on. */\n toolCallId: string;\n toolName: string;\n /**\n * Best known parsed arguments. Never the transitional `{ raw }` wrapper the\n * protocol layer emits for an argument fragment that does not parse.\n */\n toolInput: Record<string, unknown>;\n /** Argument fragments concatenated in arrival order, when the wire sent any. */\n rawArguments?: string;\n /** Whether {@link toolInput} reflects fully parsed arguments. */\n argumentsComplete: boolean;\n result?: TranscriptToolResult;\n status: TranscriptToolCallStatus;\n}\n\nexport type TranscriptRow = TranscriptTextRow | TranscriptToolCallRow;\n\n/**\n * A history page accepted by {@link TranscriptAccumulator.rebase}. Covers\n * `session.listMessages()`, `session.bootstrapState()`, and a bare array of\n * Letta API messages.\n */\nexport type TranscriptHistoryPage =\n | { messages: readonly LettaMessage[] }\n | readonly LettaMessage[];\n\nexport interface TranscriptRebaseOptions {\n /**\n * Order of the supplied page. Omitted means auto-detect from `seq_id`/`date`;\n * `listMessages()` defaults to `\"desc\"` (newest first).\n */\n order?: \"asc\" | \"desc\";\n}\n\nexport interface TranscriptAccumulator {\n /**\n * Fold one streamed message into the transcript and return the current rows.\n *\n * The returned array is referentially stable when the message changed\n * nothing (a replayed position, or a message family the accumulator ignores),\n * so it can be handed straight to a memoizing renderer.\n */\n apply(message: SDKMessage): readonly TranscriptRow[];\n /** Merge a history page into the transcript. Safe to call mid-run. */\n rebase(\n page: TranscriptHistoryPage,\n options?: TranscriptRebaseOptions,\n ): readonly TranscriptRow[];\n /** Current rows in transcript order. */\n rows(): readonly TranscriptRow[];\n /** Drop all rows and replay state. */\n reset(): void;\n}\n\n// ═══════════════════════════════════════════════════════════════\n// INTERNALS\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Key segment separator. Every key carries a family segment and an identifier\n * kind segment (\"otid\"/\"uuid\"), so no wire identifier can produce a key that\n * collides with a key from another family or another identifier kind.\n */\nconst SEP = \":\";\n\n/** Bound on tracked replay thresholds so a long session cannot grow forever. */\nconst MAX_TRACKED_RUNS = 64;\n\n/** Bucket used for streams that do not carry a `runId`. */\nconst ANONYMOUS_RUN = \"\";\n\ninterface TextSlice {\n kind: TranscriptTextKind;\n text: string;\n uuid?: string;\n otid?: string;\n runId?: string;\n seqId?: number;\n}\n\nfunction familyOtidAlias(kind: TranscriptTextKind, otid: string): string {\n return `${kind}${SEP}otid${SEP}${otid}`;\n}\n\nfunction familyUuidAlias(kind: TranscriptTextKind, uuid: string): string {\n return `${kind}${SEP}uuid${SEP}${uuid}`;\n}\n\nfunction toolRowKey(toolCallId: string): string {\n return `tool_call${SEP}id${SEP}${toolCallId}`;\n}\n\nfunction asRecord(value: unknown): Record<string, unknown> | undefined {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n}\n\nfunction readString(\n record: Record<string, unknown>,\n field: string,\n): string | undefined {\n const value = record[field];\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\nfunction readNumber(\n record: Record<string, unknown>,\n field: string,\n): number | undefined {\n const value = record[field];\n return typeof value === \"number\" && Number.isFinite(value) ? value : undefined;\n}\n\n/** Parse a JSON object, returning undefined for partial or non-object JSON. */\nfunction parseJsonObject(raw: string): Record<string, unknown> | undefined {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return undefined;\n try {\n return asRecord(JSON.parse(trimmed) as unknown);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Detect the protocol layer's transitional `{ raw }` wrapper.\n *\n * `toolInputFromArguments()` wraps an unparseable argument fragment as\n * `{ raw: \"<fragment>\" }`. That wrapper is a parse failure, not arguments, and\n * must never overwrite previously parsed input.\n */\nfunction isRawArgumentsWrapper(\n input: Record<string, unknown> | undefined,\n rawArguments: string | undefined,\n): boolean {\n if (!input) return false;\n const keys = Object.keys(input);\n if (keys.length !== 1 || keys[0] !== \"raw\") return false;\n const wrapped = input.raw;\n if (typeof wrapped !== \"string\") return false;\n return rawArguments === undefined || wrapped === rawArguments;\n}\n\nfunction toolStatus(row: {\n argumentsComplete: boolean;\n result?: TranscriptToolResult;\n}): TranscriptToolCallStatus {\n if (row.result) return \"complete\";\n return row.argumentsComplete ? \"ready\" : \"streaming\";\n}\n\ninterface ToolCallMerge {\n toolCallId: string;\n toolName?: string;\n toolInput?: Record<string, unknown>;\n rawArguments?: string;\n uuid?: string;\n runId?: string;\n seqId?: number;\n /**\n * `fragment` appends streamed argument text; `whole` treats the arguments as\n * an authoritative complete value (history backfill).\n */\n mode: \"fragment\" | \"whole\";\n}\n\ninterface ToolResultMerge {\n toolCallId: string;\n content: string;\n isError: boolean;\n uuid?: string;\n runId?: string;\n seqId?: number;\n}\n\ninterface ToolArguments {\n rawArguments?: string;\n toolInput: Record<string, unknown>;\n argumentsComplete: boolean;\n}\n\n/**\n * Fold one argument delivery into the arguments known so far.\n *\n * The wire can deliver arguments three ways for the same call: streamed JSON\n * fragments, one complete JSON string, or an already-decoded object. Only a\n * successful parse is allowed to change `toolInput`.\n */\nfunction mergeToolArguments(\n previous: ToolArguments,\n merge: ToolCallMerge,\n): ToolArguments {\n let { rawArguments, toolInput, argumentsComplete } = previous;\n const fragment = merge.rawArguments;\n const wrapped = isRawArgumentsWrapper(merge.toolInput, fragment);\n\n if (fragment !== undefined && fragment.length > 0) {\n const whole = parseJsonObject(fragment);\n if (whole) {\n // A delivery that parses on its own is the complete argument value: a\n // final non-chunked `tool_call_message`, or a backfilled history row.\n // Replace rather than append so a repeated terminal message cannot\n // corrupt the accumulation.\n return { rawArguments: fragment, toolInput: whole, argumentsComplete: true };\n }\n if (argumentsComplete) {\n // Arguments already parsed; a trailing partial (a replayed fragment after\n // backfill) must not corrupt them.\n return previous;\n }\n if (merge.mode === \"whole\") {\n return { rawArguments: rawArguments ?? fragment, toolInput, argumentsComplete };\n }\n rawArguments = (rawArguments ?? \"\") + fragment;\n const parsed = parseJsonObject(rawArguments);\n if (parsed) {\n return { rawArguments, toolInput: parsed, argumentsComplete: true };\n }\n // Keep the previous parse. Never promote the `{ raw }` wrapper.\n return { rawArguments, toolInput, argumentsComplete: false };\n }\n\n if (!wrapped && merge.toolInput) {\n const keys = Object.keys(merge.toolInput);\n if (keys.length > 0) {\n return { rawArguments, toolInput: merge.toolInput, argumentsComplete: true };\n }\n if (rawArguments === undefined) {\n // Genuinely argument-free call: `{}` with no streamed fragments.\n return { rawArguments, toolInput, argumentsComplete: true };\n }\n }\n\n return previous;\n}\n\nclass TranscriptAccumulatorImpl implements TranscriptAccumulator {\n /** Row key -> row. Map insertion order is the transcript order. */\n private byKey = new Map<string, TranscriptRow>();\n\n /** `family + otid` -> row key. */\n private aliasByOtid = new Map<string, string>();\n\n /** `family + uuid` -> row key. */\n private aliasByUuid = new Map<string, string>();\n\n /** `runId` -> highest accepted `seqId` for that run. */\n private seqThresholds = new Map<string, number>();\n\n private snapshot: readonly TranscriptRow[] | null = null;\n\n private anonymousCounter = 0;\n\n apply(message: SDKMessage): readonly TranscriptRow[] {\n switch (message.type) {\n case \"assistant\":\n case \"reasoning\":\n this.applyText({\n kind: message.type,\n text: message.content,\n uuid: message.uuid,\n otid: typeof message.otid === \"string\" ? message.otid : undefined,\n runId: message.runId,\n seqId: message.seqId,\n });\n break;\n case \"tool_call\":\n this.mergeToolCall({\n toolCallId: message.toolCallId,\n toolName: message.toolName,\n toolInput: message.toolInput,\n rawArguments: message.rawArguments,\n uuid: message.uuid,\n runId: message.runId,\n mode: \"fragment\",\n });\n break;\n case \"tool_result\":\n this.mergeToolResult({\n toolCallId: message.toolCallId,\n content: message.content,\n isError: message.isError,\n uuid: message.uuid,\n runId: message.runId,\n });\n break;\n case \"stream_event\":\n this.applyStreamEvent(message);\n break;\n default:\n // init/result/error/retry/queue_update/loop_status are turn-level\n // signals rather than transcript content; consumers handle them\n // directly.\n break;\n }\n return this.rows();\n }\n\n rebase(\n page: TranscriptHistoryPage,\n options?: TranscriptRebaseOptions,\n ): readonly TranscriptRow[] {\n const messages = normalizeHistoryPage(page, options?.order);\n if (messages.length === 0) return this.rows();\n\n const historyKeys: string[] = [];\n const seen = new Set<string>();\n for (const message of messages) {\n const key = this.applyHistoryMessage(message);\n if (!key || seen.has(key)) continue;\n seen.add(key);\n historyKeys.push(key);\n }\n\n this.reorder(historyKeys);\n return this.rows();\n }\n\n rows(): readonly TranscriptRow[] {\n if (!this.snapshot) {\n this.snapshot = Array.from(this.byKey.values());\n }\n return this.snapshot;\n }\n\n reset(): void {\n this.byKey.clear();\n this.aliasByOtid.clear();\n this.aliasByUuid.clear();\n this.seqThresholds.clear();\n this.anonymousCounter = 0;\n this.snapshot = null;\n }\n\n // ── replay suppression ────────────────────────────────────────\n\n /**\n * Per-run replay guard.\n *\n * Thresholds are bucketed by `runId`, so a brand new run starts with no\n * threshold (a natural reset) while a resumed run keeps suppressing the\n * positions it already delivered. Messages without a `seqId` are never\n * suppressed here: their families are deduplicated by identity instead.\n */\n private isReplay(\n runId: string | undefined,\n seqId: number | undefined,\n ): boolean {\n if (seqId === undefined) return false;\n const bucket = runId ?? ANONYMOUS_RUN;\n const threshold = this.seqThresholds.get(bucket);\n if (threshold !== undefined && seqId <= threshold) return true;\n this.rememberSeq(bucket, seqId);\n return false;\n }\n\n private rememberSeq(bucket: string, seqId: number): void {\n const threshold = this.seqThresholds.get(bucket);\n if (threshold !== undefined && threshold >= seqId) return;\n this.seqThresholds.set(bucket, seqId);\n while (this.seqThresholds.size > MAX_TRACKED_RUNS) {\n const oldest = this.seqThresholds.keys().next();\n if (oldest.done) break;\n this.seqThresholds.delete(oldest.value);\n }\n }\n\n // ── text families ─────────────────────────────────────────────\n\n private applyText(slice: TextSlice): void {\n if (this.isReplay(slice.runId, slice.seqId)) return;\n this.writeText(slice, \"append\");\n }\n\n private writeText(slice: TextSlice, write: \"append\" | \"replace\"): string {\n const key = this.resolveTextKey(slice.kind, slice.uuid, slice.otid);\n const existing = this.byKey.get(key);\n const previous =\n existing && existing.kind === slice.kind ? existing : undefined;\n this.setRow(key, {\n kind: slice.kind,\n key,\n text: write === \"append\" ? (previous?.text ?? \"\") + slice.text : slice.text,\n uuid: previous?.uuid ?? slice.uuid,\n otid: slice.otid ?? previous?.otid,\n runId: slice.runId ?? previous?.runId,\n seqId: maxDefined(slice.seqId, previous?.seqId),\n });\n return key;\n }\n\n /**\n * Resolve the row key for a text slice.\n *\n * `otid` is the lineage key when present, but a stream can transition: early\n * fragments may carry only the message id and later fragments add an `otid`.\n * Both identifiers are aliased to one row so the transition does not split\n * it. When a message id is reused by a *second* slice carrying a different\n * `otid` (a provider emitting `[text, thinking, text]` under one message id),\n * the new `otid` opens its own row instead of appending to the previous one.\n */\n private resolveTextKey(\n kind: TranscriptTextKind,\n uuid: string | undefined,\n otid: string | undefined,\n ): string {\n const otidAlias = otid ? familyOtidAlias(kind, otid) : undefined;\n const uuidAlias = uuid ? familyUuidAlias(kind, uuid) : undefined;\n const fromOtid = otidAlias ? this.aliasByOtid.get(otidAlias) : undefined;\n const fromUuid = uuidAlias ? this.aliasByUuid.get(uuidAlias) : undefined;\n\n let key: string | undefined = fromOtid ?? fromUuid;\n\n if (key && !fromOtid && otid) {\n const existing = this.byKey.get(key);\n if (existing && existing.otid !== undefined && existing.otid !== otid) {\n // The envelope already committed to a different lineage: this is a new\n // slice sharing a message id, not a continuation of the old one.\n key = undefined;\n }\n }\n\n if (!key) {\n key =\n otidAlias ??\n uuidAlias ??\n `${kind}${SEP}auto${SEP}${++this.anonymousCounter}`;\n }\n\n if (otidAlias) this.aliasByOtid.set(otidAlias, key);\n // The newest slice owns the envelope, so later id-only fragments continue\n // it rather than the slice that closed before it.\n if (uuidAlias) this.aliasByUuid.set(uuidAlias, key);\n\n return key;\n }\n\n // ── tool families ─────────────────────────────────────────────\n\n private mergeToolCall(merge: ToolCallMerge): string {\n const key = toolRowKey(merge.toolCallId);\n const existing = this.byKey.get(key);\n const previous =\n existing && existing.kind === \"tool_call\" ? existing : undefined;\n\n const args = mergeToolArguments(\n {\n rawArguments: previous?.rawArguments,\n toolInput: previous?.toolInput ?? {},\n argumentsComplete: previous?.argumentsComplete ?? false,\n },\n merge,\n );\n\n const toolName =\n merge.toolName && merge.toolName !== \"?\"\n ? merge.toolName\n : (previous?.toolName ?? merge.toolName ?? \"?\");\n\n const next: TranscriptToolCallRow = {\n kind: \"tool_call\",\n key,\n toolCallId: merge.toolCallId,\n toolName,\n toolInput: args.toolInput,\n ...(args.rawArguments !== undefined\n ? { rawArguments: args.rawArguments }\n : {}),\n argumentsComplete: args.argumentsComplete,\n ...(previous?.result ? { result: previous.result } : {}),\n status: \"streaming\",\n // Envelope identity stays pinned to the `tool_call` message that opened\n // the row; the payload identity is `toolCallId`.\n uuid: previous?.uuid ?? merge.uuid,\n runId: merge.runId ?? previous?.runId,\n seqId: maxDefined(merge.seqId, previous?.seqId),\n };\n next.status = toolStatus(next);\n this.setRow(key, next);\n return key;\n }\n\n private mergeToolResult(merge: ToolResultMerge): string {\n const key = toolRowKey(merge.toolCallId);\n const existing = this.byKey.get(key);\n const previous =\n existing && existing.kind === \"tool_call\" ? existing : undefined;\n\n this.setRow(key, {\n kind: \"tool_call\",\n key,\n toolCallId: merge.toolCallId,\n toolName: previous?.toolName ?? \"?\",\n toolInput: previous?.toolInput ?? {},\n ...(previous?.rawArguments !== undefined\n ? { rawArguments: previous.rawArguments }\n : {}),\n argumentsComplete: previous?.argumentsComplete ?? false,\n result: {\n content: merge.content,\n isError: merge.isError,\n // The result envelope is a different message than the call envelope,\n // so it is recorded beside the row's `uuid`, not over it.\n ...(merge.uuid ? { uuid: merge.uuid } : {}),\n },\n status: \"complete\",\n uuid: previous?.uuid,\n runId: merge.runId ?? previous?.runId,\n seqId: maxDefined(merge.seqId, previous?.seqId),\n });\n return key;\n }\n\n // ── raw stream events ─────────────────────────────────────────\n\n /**\n * Compose with {@link extractStreamTextDelta} for payloads the session layer\n * passes through uncooked.\n *\n * Identity comes from the payload when it has any (`id`, `otid`, `seq_id`,\n * `run_id`). Content-block deltas carry none, so they fold into a single live\n * row per family — the most an anonymous delta stream can support.\n */\n private applyStreamEvent(message: SDKStreamEventMessage): void {\n const delta = extractStreamTextDelta(message.event);\n if (!delta) return;\n const payload = asRecord(message.event);\n const otid = payload ? readString(payload, \"otid\") : undefined;\n const payloadId = payload ? readString(payload, \"id\") : undefined;\n const identified = otid !== undefined || payloadId !== undefined;\n\n this.applyText({\n kind: delta.kind,\n text: delta.text,\n uuid: identified ? payloadId : `${delta.kind}${SEP}live`,\n otid,\n runId: payload ? readString(payload, \"run_id\") : undefined,\n seqId: payload ? readNumber(payload, \"seq_id\") : undefined,\n });\n }\n\n // ── history backfill ──────────────────────────────────────────\n\n private applyHistoryMessage(message: LettaMessage): string | undefined {\n const record = asRecord(message);\n if (!record) return undefined;\n const messageType = readString(record, \"message_type\");\n if (!messageType) return undefined;\n\n const uuid = readString(record, \"id\");\n const otid = readString(record, \"otid\");\n const runId = readString(record, \"run_id\");\n const seqId = readNumber(record, \"seq_id\");\n\n // A history page proves every position up to its own cursor for that run,\n // so replayed deltas at or below it are suppressed after the merge.\n if (seqId !== undefined) {\n this.rememberSeq(runId ?? ANONYMOUS_RUN, seqId);\n }\n\n switch (messageType) {\n case \"user_message\":\n case \"assistant_message\": {\n const text = extractTextFromContent(record.content);\n if (text === null) return undefined;\n return this.writeText(\n {\n kind: messageType === \"user_message\" ? \"user\" : \"assistant\",\n text,\n uuid,\n otid,\n runId,\n seqId,\n },\n \"replace\",\n );\n }\n case \"reasoning_message\": {\n const text =\n typeof record.reasoning === \"string\"\n ? record.reasoning\n : extractTextFromContent(record.content);\n if (text === null || text === undefined) return undefined;\n return this.writeText(\n { kind: \"reasoning\", text, uuid, otid, runId, seqId },\n \"replace\",\n );\n }\n case \"tool_call_message\":\n case \"approval_request_message\": {\n const toolCall = firstToolCall(record);\n if (!toolCall) return undefined;\n const fn = asRecord(toolCall.function);\n const toolCallId =\n (typeof toolCall.tool_call_id === \"string\"\n ? toolCall.tool_call_id\n : undefined) ??\n (typeof toolCall.id === \"string\" ? toolCall.id : undefined);\n if (!toolCallId) return undefined;\n const args = toolCall.arguments ?? fn?.arguments;\n return this.mergeToolCall({\n toolCallId,\n toolName:\n (typeof toolCall.name === \"string\" ? toolCall.name : undefined) ??\n (typeof fn?.name === \"string\" ? fn.name : undefined),\n toolInput: asRecord(args),\n rawArguments: typeof args === \"string\" ? args : undefined,\n uuid,\n runId,\n seqId,\n mode: \"whole\",\n });\n }\n case \"tool_return_message\": {\n const toolReturn = firstToolReturn(record) ?? record;\n const toolCallId =\n readString(record, \"tool_call_id\") ??\n (typeof toolReturn.tool_call_id === \"string\"\n ? toolReturn.tool_call_id\n : undefined);\n if (!toolCallId) return undefined;\n const content =\n extractTextFromContent(\n record.tool_return ?? toolReturn.tool_return ?? toolReturn.content,\n ) ?? \"\";\n const status =\n readString(record, \"status\") ??\n (typeof toolReturn.status === \"string\"\n ? toolReturn.status\n : undefined);\n return this.mergeToolResult({\n toolCallId,\n content,\n isError: status === \"error\",\n uuid,\n runId,\n seqId,\n });\n }\n default:\n // system/summary/event/hidden-reasoning/approval-response messages are\n // not transcript content this accumulator claims to own.\n return undefined;\n }\n }\n\n // ── row bookkeeping ───────────────────────────────────────────\n\n private setRow(key: string, row: TranscriptRow): void {\n this.byKey.set(key, row);\n this.snapshot = null;\n }\n\n /** Move backfilled rows ahead of rows that only exist in the live stream. */\n private reorder(historyKeys: readonly string[]): void {\n if (historyKeys.length === 0) return;\n const historySet = new Set(historyKeys);\n const reordered = new Map<string, TranscriptRow>();\n for (const key of historyKeys) {\n const row = this.byKey.get(key);\n if (row) reordered.set(key, row);\n }\n for (const [key, row] of this.byKey) {\n if (historySet.has(key)) continue;\n reordered.set(key, row);\n }\n this.byKey = reordered;\n this.snapshot = null;\n }\n}\n\nfunction maxDefined(\n next: number | undefined,\n previous: number | undefined,\n): number | undefined {\n if (next === undefined) return previous;\n if (previous === undefined) return next;\n return Math.max(next, previous);\n}\n\nfunction normalizeHistoryPage(\n page: TranscriptHistoryPage,\n order: \"asc\" | \"desc\" | undefined,\n): readonly LettaMessage[] {\n const messages = Array.isArray(page)\n ? (page as readonly LettaMessage[])\n : ((page as { messages?: readonly LettaMessage[] }).messages ?? []);\n if (messages.length < 2) return messages;\n const descending = order ? order === \"desc\" : detectDescending(messages);\n return descending ? [...messages].reverse() : messages;\n}\n\n/**\n * `listMessages()` defaults to newest-first. Detect that from the page itself\n * so callers do not have to restate the order they requested.\n */\nfunction detectDescending(messages: readonly LettaMessage[]): boolean {\n const seqIds: number[] = [];\n const dates: number[] = [];\n for (const message of messages) {\n const record = asRecord(message);\n if (!record) continue;\n const seqId = readNumber(record, \"seq_id\");\n if (seqId !== undefined) seqIds.push(seqId);\n const date = readString(record, \"date\");\n if (!date) continue;\n const parsed = Date.parse(date);\n if (!Number.isNaN(parsed)) dates.push(parsed);\n }\n const ordered = seqIds.length >= 2 ? seqIds : dates;\n if (ordered.length < 2) return false;\n const first = ordered[0] as number;\n const last = ordered[ordered.length - 1] as number;\n return last < first;\n}\n\n/**\n * Create a transcript accumulator.\n *\n * @example\n * ```typescript\n * const acc = createTranscriptAccumulator();\n * for await (const message of session.stream()) {\n * render(acc.apply(message));\n * }\n *\n * // Safe mid-run: merges older history without duplicating live rows.\n * acc.rebase(await session.listMessages({ limit: 50 }));\n * ```\n */\nexport function createTranscriptAccumulator(): TranscriptAccumulator {\n return new TranscriptAccumulatorImpl();\n}\n",
|
|
173
173
|
"import type {\n LettaCodeReactNativeSocketConstructor,\n LettaCodeSocketConstructor,\n LettaCodeSocketOptions,\n} from \"./types.js\";\n\n/**\n * Adapt React Native's three-argument WebSocket constructor to the SDK's\n * Node-compatible constructor shape. This is only needed when a remote\n * app-server authenticates its websocket upgrade with headers. Cloud clients\n * should prefer `webSocketAuth: \"query\"`.\n */\nexport function createReactNativeWebSocketConstructor(\n WebSocket: LettaCodeReactNativeSocketConstructor,\n): LettaCodeSocketConstructor {\n class ReactNativeWebSocketAdapter {\n constructor(url: string, options?: LettaCodeSocketOptions) {\n return new WebSocket(url, undefined, options);\n }\n }\n return ReactNativeWebSocketAdapter as unknown as LettaCodeSocketConstructor;\n}\n",
|
package/dist/client.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ import type { QueryOptions } from "./query-types.js";
|
|
|
4
4
|
import type { SkillNodeSupport } from "./skill-loading.js";
|
|
5
5
|
import type { CreateAgentOptions, LettaCodeClientSessionOptions, LettaCodeSession } from "./types.js";
|
|
6
6
|
export declare class LettaAgentClient extends LettaAgentClientBase {
|
|
7
|
+
private localServer;
|
|
8
|
+
private localClosePromise;
|
|
9
|
+
private sharedLocalServer;
|
|
10
|
+
close(): Promise<void>;
|
|
7
11
|
protected skillNodeSupport(): SkillNodeSupport;
|
|
8
12
|
protected createLocalManagementTransport(): ManagementTransport;
|
|
9
13
|
protected createLocalAgent(options: CreateAgentOptions): Promise<string>;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAG3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EACV,kBAAkB,EAClB,6BAA6B,EAE7B,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,gBAAiB,SAAQ,oBAAoB;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAG3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EACV,kBAAkB,EAClB,6BAA6B,EAE7B,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,gBAAiB,SAAQ,oBAAoB;IACxD,OAAO,CAAC,WAAW,CAAqC;IACxD,OAAO,CAAC,iBAAiB,CAA8B;IAEvD,OAAO,CAAC,iBAAiB;IAUhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;cAUZ,gBAAgB,IAAI,gBAAgB;cAIpC,8BAA8B,IAAI,mBAAmB;cAoB/C,gBAAgB,CACvC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,MAAM,CAAC;cAqBC,kBAAkB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,6BAA6B,GACrC,gBAAgB;cAcA,uBAAuB,CACxC,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,6BAA6B,GAC5C,gBAAgB;cAgCA,kBAAkB,CACnC,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,6BAA6B,GACrC,gBAAgB;CAwBpB"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAOH,OAAO,KAAK,EACV,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAEhB,gBAAgB,EAChB,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAI3D,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,EAC3B,8BAA8B,EAC9B,4BAA4B,EAC5B,2BAA2B,EAC3B,4BAA4B,EAC5B,mBAAmB,EACnB,sBAAsB,EACtB,6BAA6B,EAC7B,gBAAgB,EAChB,mBAAmB,EACnB,0BAA0B,EAC1B,qCAAqC,EACrC,UAAU,EACV,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,qBAAqB,EACrB,qBAAqB,EACrB,0BAA0B,EAC1B,4BAA4B,EAC5B,4BAA4B,EAC5B,eAAe,EACf,eAAe,EACf,YAAY,EACZ,qBAAqB,EACrB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,8BAA8B,EAC9B,6BAA6B,EAC7B,wBAAwB,EACxB,yBAAyB,EACzB,sBAAsB,EACtB,mBAAmB,EACnB,4BAA4B,EAC5B,gBAAgB,EAChB,2BAA2B,EAC3B,mBAAmB,EACnB,eAAe,EACf,kBAAkB,EAClB,WAAW,EACX,SAAS,EACT,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,eAAe,EACf,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,8BAA8B,EAC9B,kBAAkB,EAClB,uBAAuB,EACvB,sBAAsB,EAEtB,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,sBAAsB,EACtB,YAAY,EAEZ,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,sBAAsB,EACtB,sBAAsB,EACtB,sBAAsB,EACtB,kBAAkB,EAClB,mBAAmB,EACnB,yBAAyB,EACzB,yBAAyB,EACzB,0BAA0B,EAC1B,cAAc,EACd,0BAA0B,EAC1B,4BAA4B,EAC5B,0BAA0B,EAC1B,0BAA0B,EAC1B,iBAAiB,EACjB,4BAA4B,EAC5B,0BAA0B,EAE1B,qBAAqB,EACrB,oBAAoB,EAEpB,SAAS,EACT,eAAe,EACf,sBAAsB,EACtB,uBAAuB,EACvB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,kBAAkB,EAClB,UAAU,GACX,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,qBAAqB,EACrB,KAAK,EACL,YAAY,EACZ,WAAW,GACZ,MAAM,kBAAkB,CAAC;AAC1B,mBAAmB,oBAAoB,CAAC;AACxC,YAAY,EACV,uBAAuB,EACvB,eAAe,EACf,8BAA8B,EAC9B,4BAA4B,EAC5B,YAAY,EACZ,mBAAmB,EACnB,UAAU,EACV,iBAAiB,EACjB,wBAAwB,EACxB,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,yBAAyB,EACzB,uBAAuB,EACvB,yBAAyB,EACzB,2BAA2B,EAC3B,0BAA0B,EAC1B,4BAA4B,EAC5B,0BAA0B,EAC1B,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,8BAA8B,EAAE,MAAM,wBAAwB,CAAC;AACxE,YAAY,EACV,QAAQ,EACR,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,+BAA+B,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,qCAAqC,EAAE,MAAM,gBAAgB,CAAC;AAEvE,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAC;AAC1E,YAAY,EACV,qBAAqB,EACrB,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EACL,UAAU,EACV,eAAe,EACf,eAAe,EACf,gBAAgB,EAChB,oBAAoB,GACrB,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|