feat: normalize stream_event contract + add live integration coverage (#52)

This commit is contained in:
Charles Packer
2026-02-22 22:44:00 -08:00
committed by GitHub
parent 4a6da241fb
commit 63317a234f
15 changed files with 14454 additions and 14 deletions

View File

@@ -77,3 +77,38 @@ const session = createSession("agent-123", {
---
Made with 💜 in San Francisco
## Live integration tests (opt-in)
The SDK includes live integration tests that hit real Letta Cloud endpoints and verify runtime contracts for:
- session init shape
- send/stream lifecycle (`assistant`, `reasoning`, `stream_event`, `result`)
- `listMessages()` backfill/pagination shape
- concurrent `listMessages()` during active stream
- tool lifecycle (`tool_call` -> `tool_result`)
These tests are opt-in and skipped by default.
```bash
# Required
export LETTA_API_KEY=sk-let-...
# Optional
export LETTA_AGENT_ID=agent-... # force a specific agent
export LETTA_CONVERSATION_ID=conv-... # force a specific conversation for init test
export LETTA_BASE_URL=https://api.letta.com
export LETTA_LIVE_TEST_TIMEOUT_MS=180000
# Run live tests
bun run test:live
# Run and record sanitized fixtures to src/tests/fixtures/live/
bun run test:live:record
```
Safety notes:
- live tests create/use real conversations on the target account
- fixture recording redacts obvious secrets/tokens and local home paths
- keep fixture recording disabled in CI unless you explicitly want refreshed snapshots

View File

@@ -19,7 +19,9 @@
"build": "bun run build.ts",
"dev": "bun run build.ts --watch",
"check": "tsc --noEmit",
"test": "bun test"
"test": "bun test",
"test:live": "LETTA_LIVE_INTEGRATION=1 bun test src/tests/live.integration.test.ts",
"test:live:record": "LETTA_LIVE_INTEGRATION=1 LETTA_RECORD_FIXTURES=1 bun test src/tests/live.integration.test.ts"
},
"repository": {
"type": "git",

View File

@@ -44,6 +44,10 @@ export type {
SDKReasoningMessage,
SDKResultMessage,
SDKStreamEventMessage,
SDKStreamEventPayload,
SDKStreamEventDeltaPayload,
SDKStreamEventMessagePayload,
SDKUnknownStreamEventPayload,
SDKErrorMessage,
SDKRetryMessage,
SkillSource,
@@ -74,6 +78,8 @@ export type {
export { Session } from "./session.js";
export { extractStreamTextDelta } from "./stream-events.js";
// Tool helpers
export {
jsonResult,

View File

@@ -24,6 +24,7 @@ import type {
ExecuteExternalToolRequest,
ListMessagesOptions,
ListMessagesResult,
SDKStreamEventPayload,
} from "./types.js";
import {
isHeadlessAutoAllowTool,
@@ -780,17 +781,13 @@ export class Session implements AsyncDisposable {
// Stream event (partial message updates)
if (wireMsg.type === "stream_event") {
const msg = wireMsg as WireMessage & {
event: {
type: string;
index?: number;
delta?: { type?: string; text?: string; reasoning?: string };
content_block?: { type?: string; text?: string };
};
event: unknown;
uuid: string;
};
const eventPayload = (msg.event ?? {}) as SDKStreamEventPayload;
return {
type: "stream_event",
event: msg.event,
event: eventPayload,
uuid: msg.uuid,
};
}

88
src/stream-events.ts Normal file
View File

@@ -0,0 +1,88 @@
import type { SDKStreamEventPayload } from "./types.js";
export type StreamTextKind = "assistant" | "reasoning";
export interface StreamTextDelta {
kind: StreamTextKind;
text: string;
}
function extractTextFromContent(content: unknown): string | null {
if (typeof content === "string") {
return content;
}
if (Array.isArray(content)) {
const pieces: string[] = [];
for (const part of content) {
if (typeof part === "string") {
pieces.push(part);
continue;
}
if (!part || typeof part !== "object") {
continue;
}
const rec = part as Record<string, unknown>;
if (typeof rec.text === "string") {
pieces.push(rec.text);
}
}
const joined = pieces.join("");
return joined.length > 0 ? joined : null;
}
if (content && typeof content === "object") {
const rec = content as Record<string, unknown>;
if (typeof rec.text === "string") {
return rec.text;
}
}
return null;
}
/**
* Extract appendable assistant/reasoning text from a stream_event payload.
*
* Supports both shapes currently emitted by headless mode:
* 1) content_block style: { type, delta: { text|reasoning } }
* 2) message chunk style: { message_type: "assistant_message"|"reasoning_message", ... }
*/
export function extractStreamTextDelta(event: SDKStreamEventPayload): StreamTextDelta | null {
if (!event || typeof event !== "object") {
return null;
}
const rec = event as Record<string, unknown>;
const maybeDelta = rec.delta;
if (maybeDelta && typeof maybeDelta === "object") {
const delta = maybeDelta as Record<string, unknown>;
if (typeof delta.reasoning === "string" && delta.reasoning.length > 0) {
return { kind: "reasoning", text: delta.reasoning };
}
if (typeof delta.text === "string" && delta.text.length > 0) {
return { kind: "assistant", text: delta.text };
}
}
const messageType = rec.message_type;
if (messageType === "reasoning_message") {
const reasoningText =
typeof rec.reasoning === "string" ? rec.reasoning : extractTextFromContent(rec.content);
if (reasoningText && reasoningText.length > 0) {
return { kind: "reasoning", text: reasoningText };
}
}
if (messageType === "assistant_message") {
const assistantText = extractTextFromContent(rec.content);
if (assistantText && assistantText.length > 0) {
return { kind: "assistant", text: assistantText };
}
}
return null;
}

View File

@@ -0,0 +1,39 @@
{
"selectedAgentName": "letta-code-agent",
"init": {
"type": "init",
"agentId": "agent-d0ab297f-c180-4347-b3ab-20165fbe0f2f",
"sessionId": "agent-d0ab297f-c180-4347-b3ab-20165fbe0f2f",
"conversationId": "conv-d87b461c-e855-49d6-9688-f69e3864f44e",
"model": "claude-sonnet-4-5-20250929",
"tools": [
"AskUserQuestion",
"Bash",
"TaskOutput",
"Edit",
"EnterPlanMode",
"ExitPlanMode",
"Glob",
"Grep",
"TaskStop",
"Read",
"Skill",
"Task",
"TodoWrite",
"Write"
],
"memfsEnabled": false,
"skillSources": [
"bundled",
"global",
"agent",
"project"
],
"systemInfoReminderEnabled": true,
"sleeptime": {
"trigger": "step-count",
"behavior": "reminder",
"stepCount": 25
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,51 @@
{
"init": {
"type": "init",
"agentId": "agent-d0ab297f-c180-4347-b3ab-20165fbe0f2f",
"sessionId": "agent-d0ab297f-c180-4347-b3ab-20165fbe0f2f",
"conversationId": "conv-3b89b7f3-c9b9-4ae2-94c5-55e3e5d44234",
"model": "claude-sonnet-4-5-20250929",
"tools": [
"AskUserQuestion",
"Bash",
"TaskOutput",
"Edit",
"EnterPlanMode",
"ExitPlanMode",
"Glob",
"Grep",
"TaskStop",
"Read",
"Skill",
"Task",
"TodoWrite",
"Write"
],
"memfsEnabled": false,
"skillSources": [
"bundled",
"global",
"agent",
"project"
],
"systemInfoReminderEnabled": true,
"sleeptime": {
"trigger": "step-count",
"behavior": "reminder",
"stepCount": 25
}
},
"page1Summary": {
"count": 4,
"hasMore": false,
"nextBefore": "message-2fcbada1-fd32-4004-88cb-395bc7cf1e88",
"sampleId": "message-5c16d0db-1c1c-4e5c-ac66-30f72367759c",
"sampleDiscriminator": "reasoning_message"
},
"page2Summary": {
"count": 0,
"hasMore": false,
"nextBefore": null,
"sampleId": null
}
}

View File

@@ -0,0 +1,36 @@
{
"type": "init",
"agentId": "agent-d0ab297f-c180-4347-b3ab-20165fbe0f2f",
"sessionId": "agent-d0ab297f-c180-4347-b3ab-20165fbe0f2f",
"conversationId": "conv-d87b461c-e855-49d6-9688-f69e3864f44e",
"model": "claude-sonnet-4-5-20250929",
"tools": [
"AskUserQuestion",
"Bash",
"TaskOutput",
"Edit",
"EnterPlanMode",
"ExitPlanMode",
"Glob",
"Grep",
"TaskStop",
"Read",
"Skill",
"Task",
"TodoWrite",
"Write"
],
"memfsEnabled": false,
"skillSources": [
"bundled",
"global",
"agent",
"project"
],
"systemInfoReminderEnabled": true,
"sleeptime": {
"trigger": "step-count",
"behavior": "reminder",
"stepCount": 25
}
}

View File

@@ -0,0 +1,483 @@
{
"init": {
"type": "init",
"agentId": "agent-d0ab297f-c180-4347-b3ab-20165fbe0f2f",
"sessionId": "agent-d0ab297f-c180-4347-b3ab-20165fbe0f2f",
"conversationId": "conv-5567c301-1ce6-4eee-a4a6-a4ac62e088f1",
"model": "claude-sonnet-4-5-20250929",
"tools": [
"AskUserQuestion",
"Bash",
"TaskOutput",
"Edit",
"EnterPlanMode",
"ExitPlanMode",
"Glob",
"Grep",
"TaskStop",
"Read",
"Skill",
"Task",
"TodoWrite",
"Write"
],
"memfsEnabled": false,
"skillSources": [
"bundled",
"global",
"agent",
"project"
],
"systemInfoReminderEnabled": true,
"sleeptime": {
"trigger": "step-count",
"behavior": "reminder",
"stepCount": 25
}
},
"prompt": "Reply with exactly this text and nothing else: SDK_LIVE_OK_opmuof",
"messages": [
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:47+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 1,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": "The user is asking me to reply",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 2,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " with exactly \"",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 3,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": "SDK_LIVE_OK_op",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 4,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": "muof\" and",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 5,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " nothing else. This appears to be a",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 6,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " test or",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 7,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " verification",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 8,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " message,",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 9,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " likely",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 10,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " for",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 11,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " the SDK",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 12,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " integration",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 13,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": ". I should",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 14,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " respond with",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 15,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " exactly",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 16,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " that",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 17,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " text and no",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 18,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " additional",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "reasoning_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1280",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 19,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"source": "reasoner_model",
"reasoning": " content.",
"signature": null
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1281",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:48+00:00",
"name": null,
"message_type": "assistant_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1281",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 20,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"content": "SDK"
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1281",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:49+00:00",
"name": null,
"message_type": "assistant_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1281",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 21,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"content": "_LIVE_OK_opmu"
}
},
{
"type": "stream_event",
"uuid": "7f876da4-3260-4ce3-b5d3-876f63ce1281",
"event": {
"id": "message-7f876da4-3260-4ce3-b5d3-876f63ce12f9",
"date": "2026-02-23T06:04:49+00:00",
"name": null,
"message_type": "assistant_message",
"otid": "7f876da4-3260-4ce3-b5d3-876f63ce1281",
"sender_id": null,
"step_id": "step-6695658c-942c-48f8-987b-79fcd41a4368",
"is_err": null,
"seq_id": 22,
"run_id": "run-fd4a4e96-b8c1-4d69-bbc0-7b4371f5fd14",
"content": "of"
}
},
{
"type": "stream_event",
"uuid": "e92869da-9f7c-453d-9e5a-e73b3e8c4369",
"event": {
"message_type": "stop_reason",
"stop_reason": "end_turn"
}
},
{
"type": "stream_event",
"uuid": "85663d3a-0a18-47b9-9c7c-7986cf4eb629",
"event": {
"message_type": "usage_statistics",
"completion_tokens": "<redacted>",
"prompt_tokens": "<redacted>",
"total_tokens": "<redacted>",
"step_count": 1,
"run_ids": null,
"cached_input_tokens": "<redacted>",
"cache_write_tokens": "<redacted>",
"reasoning_tokens": "<redacted>",
"context_tokens": "<redacted>"
}
},
{
"type": "result",
"success": true,
"durationMs": 5680,
"conversationId": "conv-5567c301-1ce6-4eee-a4a6-a4ac62e088f1"
}
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,552 @@
import { afterAll, describe, expect, test } from "bun:test";
import { createSession, resumeSession, type Session } from "../index.js";
import type {
SDKMessage,
SDKResultMessage,
SDKStreamEventMessage,
ListMessagesResult,
} from "../types.js";
import { mkdir, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const API_KEY = process.env.LETTA_API_KEY;
const RUN_LIVE = process.env.LETTA_LIVE_INTEGRATION === "1" && !!API_KEY;
const RECORD_FIXTURES = process.env.LETTA_RECORD_FIXTURES === "1";
const BASE_URL = process.env.LETTA_BASE_URL ?? "https://api.letta.com";
const AGENT_ID_OVERRIDE = process.env.LETTA_AGENT_ID;
const CONVERSATION_ID_OVERRIDE = process.env.LETTA_CONVERSATION_ID;
const TEST_TIMEOUT_MS = Number(process.env.LETTA_LIVE_TEST_TIMEOUT_MS ?? "180000");
const describeLive = RUN_LIVE ? describe : describe.skip;
type AgentSummary = {
id?: string;
name?: string;
tools?: Array<{ name?: string }>;
tags?: string[];
};
let agentId = "";
let selectedAgentName = "";
let seededConversationId = "";
let ensureReadyPromise: Promise<void> | null = null;
const openedSessions: Session[] = [];
function nowIso(): string {
return new Date().toISOString();
}
function log(message: string, data?: unknown): void {
if (data === undefined) {
console.log(`[live-sdk:${nowIso()}] ${message}`);
return;
}
console.log(`[live-sdk:${nowIso()}] ${message}`, data);
}
function hasTool(agent: AgentSummary, toolName: string): boolean {
return !!agent.tools?.some((t) => t?.name === toolName);
}
function pickBestAgent(agents: AgentSummary[]): AgentSummary | null {
if (agents.length === 0) return null;
const byNameAndBash = agents.find(
(a) => /big\s*chungus|lettabot/i.test(a.name ?? "") && hasTool(a, "Bash"),
);
if (byNameAndBash) return byNameAndBash;
const byTagAndBash = agents.find(
(a) => (a.tags ?? []).includes("origin:letta-code") && hasTool(a, "Bash"),
);
if (byTagAndBash) return byTagAndBash;
const byBash = agents.find((a) => hasTool(a, "Bash"));
if (byBash) return byBash;
return agents.find((a) => typeof a.id === "string") ?? null;
}
async function discoverAgent(): Promise<{ id: string; name: string }> {
if (AGENT_ID_OVERRIDE) return { id: AGENT_ID_OVERRIDE, name: "override" };
if (!API_KEY) throw new Error("LETTA_API_KEY is required");
const response = await fetch(`${BASE_URL}/v1/agents?limit=200`, {
headers: { Authorization: `Bearer ${API_KEY}` },
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to discover agent: ${response.status} ${text}`);
}
const payload = (await response.json()) as AgentSummary[];
if (!Array.isArray(payload) || payload.length === 0) {
throw new Error("No agents found for LETTA_API_KEY");
}
const picked = pickBestAgent(payload);
if (!picked?.id) throw new Error("Could not pick a valid agent from discovered agents");
return { id: picked.id, name: picked.name ?? "unnamed" };
}
async function ensureAgentReady(): Promise<void> {
if (agentId) return;
if (!ensureReadyPromise) {
ensureReadyPromise = (async () => {
const discovered = await discoverAgent();
agentId = discovered.id;
selectedAgentName = discovered.name;
log(`using agentId=${agentId} (${selectedAgentName})`);
})();
}
await ensureReadyPromise;
}
function redactString(value: string): string {
return value
.replace(/sk-let-[A-Za-z0-9:=+/_-]+/g, "<redacted-api-key>")
.replace(/\/Users\/[^\s"']+/g, "/Users/<redacted>");
}
function sanitizeValue(value: unknown): unknown {
if (typeof value === "string") return redactString(value);
if (Array.isArray(value)) return value.map(sanitizeValue);
if (value && typeof value === "object") {
const out: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
if (k.toLowerCase().includes("token") || k.toLowerCase().includes("authorization")) {
out[k] = "<redacted>";
} else {
out[k] = sanitizeValue(v);
}
}
return out;
}
return value;
}
function summarizeMessage(message: SDKMessage): Record<string, unknown> {
const base: Record<string, unknown> = { type: message.type };
if ("uuid" in message && typeof message.uuid === "string") {
base.uuid = message.uuid;
}
switch (message.type) {
case "assistant":
case "reasoning":
base.contentPreview = message.content.slice(0, 160);
base.contentLength = message.content.length;
break;
case "tool_call":
base.toolCallId = message.toolCallId;
base.toolName = message.toolName;
base.toolInput = sanitizeValue(message.toolInput);
break;
case "tool_result":
base.toolCallId = message.toolCallId;
base.isError = message.isError;
base.contentPreview = message.content.slice(0, 160);
base.contentLength = message.content.length;
break;
case "stream_event":
base.event = sanitizeValue(message.event);
break;
case "result":
base.success = message.success;
base.error = message.error;
base.stopReason = message.stopReason;
base.durationMs = message.durationMs;
base.conversationId = message.conversationId;
break;
case "error":
base.message = message.message;
base.stopReason = message.stopReason;
base.runId = message.runId;
base.apiError = sanitizeValue(message.apiError);
break;
case "retry":
base.reason = message.reason;
base.attempt = message.attempt;
base.maxAttempts = message.maxAttempts;
base.delayMs = message.delayMs;
base.runId = message.runId;
break;
case "init":
base.agentId = message.agentId;
base.sessionId = message.sessionId;
base.conversationId = message.conversationId;
base.model = message.model;
break;
}
return base;
}
async function writeFixture(name: string, body: unknown): Promise<void> {
if (!RECORD_FIXTURES) return;
const thisFile = fileURLToPath(import.meta.url);
const fixtureDir = join(dirname(thisFile), "fixtures", "live");
await mkdir(fixtureDir, { recursive: true });
const target = join(fixtureDir, `${name}.json`);
await writeFile(target, JSON.stringify(sanitizeValue(body), null, 2), "utf8");
log(`wrote fixture ${target}`);
}
async function collectTurn(session: Session, prompt: string): Promise<SDKMessage[]> {
await session.send(prompt);
const messages: SDKMessage[] = [];
const start = Date.now();
for await (const message of session.stream()) {
messages.push(message);
if (Date.now() - start > TEST_TIMEOUT_MS) {
throw new Error(`stream timed out after ${TEST_TIMEOUT_MS}ms`);
}
}
return messages;
}
function expectTerminalResult(messages: SDKMessage[]): SDKResultMessage {
const last = messages[messages.length - 1];
expect(last).toBeDefined();
expect(last?.type).toBe("result");
return last as SDKResultMessage;
}
function hasRenderableContent(messages: SDKMessage[]): boolean {
return messages.some((m) => m.type === "assistant" || m.type === "reasoning" || m.type === "stream_event");
}
function pickAnyMessageId(page: ListMessagesResult): string | null {
for (const item of page.messages) {
if (item && typeof item === "object") {
const obj = item as Record<string, unknown>;
if (typeof obj.id === "string") return obj.id;
}
}
return null;
}
function assertRawMessageShape(page: ListMessagesResult): void {
for (const item of page.messages) {
expect(item && typeof item === "object").toBe(true);
const obj = item as Record<string, unknown>;
const discriminator = obj.message_type ?? obj.type;
expect(typeof discriminator === "string").toBe(true);
}
}
describeLive("live integration: letta-code-sdk", () => {
afterAll(() => {
for (const session of openedSessions) {
session.close();
}
openedSessions.length = 0;
});
test(
"createSession initialize returns stable init contract",
async () => {
await ensureAgentReady();
const session = createSession(agentId, {
permissionMode: "bypassPermissions",
includePartialMessages: true,
});
openedSessions.push(session);
const init = await session.initialize();
seededConversationId = init.conversationId;
expect(init.type).toBe("init");
expect(init.agentId).toBe(agentId);
expect(init.sessionId.length).toBeGreaterThan(5);
expect(init.conversationId.startsWith("conv-")).toBe(true);
expect(Array.isArray(init.tools)).toBe(true);
await writeFixture("init_contract", {
selectedAgentName,
init,
});
},
TEST_TIMEOUT_MS,
);
test(
"resumeSession(conversationId) rehydrates existing conversation",
async () => {
await ensureAgentReady();
const conversationId = CONVERSATION_ID_OVERRIDE || seededConversationId;
expect(conversationId.startsWith("conv-")).toBe(true);
const session = resumeSession(conversationId, {
permissionMode: "bypassPermissions",
includePartialMessages: true,
});
openedSessions.push(session);
const init = await session.initialize();
expect(init.conversationId).toBe(conversationId);
await writeFixture("resume_conversation_init", init);
},
TEST_TIMEOUT_MS,
);
test(
"send + stream yields renderable messages and terminal result",
async () => {
await ensureAgentReady();
const session = createSession(agentId, {
permissionMode: "bypassPermissions",
includePartialMessages: true,
});
openedSessions.push(session);
const init = await session.initialize();
const nonce = Math.random().toString(36).slice(2, 8);
const prompt = `Reply with exactly this text and nothing else: SDK_LIVE_OK_${nonce}`;
const messages = await collectTurn(session, prompt);
const result = expectTerminalResult(messages);
expect(result.success).toBe(true);
expect(result.conversationId).toBe(init.conversationId);
expect(hasRenderableContent(messages)).toBe(true);
await writeFixture("send_stream_basic", {
init,
prompt,
messages: messages.map(summarizeMessage),
});
},
TEST_TIMEOUT_MS,
);
test(
"includePartialMessages=true contract: stream_event if available, otherwise assistant/reasoning fallback",
async () => {
await ensureAgentReady();
const session = createSession(agentId, {
permissionMode: "bypassPermissions",
includePartialMessages: true,
});
openedSessions.push(session);
const init = await session.initialize();
const prompt =
"Produce 40 short bullet points about test observability. One bullet per line.";
const messages = await collectTurn(session, prompt);
const result = expectTerminalResult(messages);
expect(result.success).toBe(true);
const streamEvents = messages.filter((m): m is SDKStreamEventMessage => m.type === "stream_event");
const deltaEvents = streamEvents.filter((m) => m.event.type === "content_block_delta");
const fallbackRenderable = messages.filter((m) => m.type === "assistant" || m.type === "reasoning");
expect(streamEvents.length + fallbackRenderable.length).toBeGreaterThan(0);
if (deltaEvents.length === 0) {
log("no stream_event delta observed on this turn; fallback render path still validated", {
selectedAgentName,
agentId,
});
}
await writeFixture("stream_event_partials", {
init,
prompt,
counts: {
total: messages.length,
streamEvents: streamEvents.length,
deltaEvents: deltaEvents.length,
assistantOrReasoning: fallbackRenderable.length,
},
messages: messages.map(summarizeMessage),
});
},
TEST_TIMEOUT_MS,
);
test(
"listMessages returns raw API messages and paginates",
async () => {
await ensureAgentReady();
const session = createSession(agentId, {
permissionMode: "bypassPermissions",
includePartialMessages: true,
});
openedSessions.push(session);
const init = await session.initialize();
await collectTurn(
session,
"Respond with two short lines proving this thread has content for backfill testing.",
);
const page1 = await session.listMessages({
conversationId: init.conversationId,
limit: 25,
order: "desc",
});
expect(Array.isArray(page1.messages)).toBe(true);
expect(page1.messages.length).toBeGreaterThan(0);
expect(typeof page1.hasMore).toBe("boolean");
assertRawMessageShape(page1);
let page2: ListMessagesResult | null = null;
if (page1.nextBefore) {
page2 = await session.listMessages({
conversationId: init.conversationId,
before: page1.nextBefore,
limit: 25,
order: "desc",
});
expect(Array.isArray(page2.messages)).toBe(true);
assertRawMessageShape(page2);
}
await writeFixture("list_messages_pagination", {
init,
page1Summary: {
count: page1.messages.length,
hasMore: page1.hasMore,
nextBefore: page1.nextBefore,
sampleId: pickAnyMessageId(page1),
sampleDiscriminator:
((page1.messages[0] as Record<string, unknown> | undefined)?.message_type as string | undefined) ??
((page1.messages[0] as Record<string, unknown> | undefined)?.type as string | undefined) ??
null,
},
page2Summary: page2
? {
count: page2.messages.length,
hasMore: page2.hasMore,
nextBefore: page2.nextBefore,
sampleId: pickAnyMessageId(page2),
}
: null,
});
},
TEST_TIMEOUT_MS,
);
test(
"listMessages is safe while stream is active",
async () => {
await ensureAgentReady();
const session = createSession(agentId, {
permissionMode: "bypassPermissions",
includePartialMessages: true,
});
openedSessions.push(session);
const init = await session.initialize();
const prompt =
"Write a medium-length response with at least 30 numbered lines describing integration test anti-patterns.";
await session.send(prompt);
const streamMessages: SDKMessage[] = [];
const streamPromise = (async () => {
for await (const m of session.stream()) {
streamMessages.push(m);
}
})();
await new Promise((resolve) => setTimeout(resolve, 250));
const page = await session.listMessages({
conversationId: init.conversationId,
limit: 10,
order: "desc",
});
expect(Array.isArray(page.messages)).toBe(true);
expect(page.messages.length).toBeGreaterThan(0);
await streamPromise;
const result = expectTerminalResult(streamMessages);
expect(result.success).toBe(true);
await writeFixture("list_messages_during_stream", {
init,
pageSummary: {
count: page.messages.length,
hasMore: page.hasMore,
nextBefore: page.nextBefore,
},
streamSummary: streamMessages.map(summarizeMessage),
});
},
TEST_TIMEOUT_MS,
);
test(
"tool lifecycle contract (best-effort): if tool_call appears, tool_result must correlate",
async () => {
await ensureAgentReady();
const session = createSession(agentId, {
permissionMode: "bypassPermissions",
includePartialMessages: true,
});
openedSessions.push(session);
await session.initialize();
const attempts: Array<{ prompt: string; messages: SDKMessage[] }> = [];
const prompts = [
"Use the Bash tool to run: echo LETTA_SDK_LIVE_TOOL_1. Then return only the command output.",
"You must invoke Bash. Run exactly: echo LETTA_SDK_LIVE_TOOL_2 and report the output.",
"Call Bash now: echo LETTA_SDK_LIVE_TOOL_3",
];
let toolCalls: Array<Extract<SDKMessage, { type: "tool_call" }>> = [];
let toolResults: Array<Extract<SDKMessage, { type: "tool_result" }>> = [];
for (const prompt of prompts) {
const messages = await collectTurn(session, prompt);
attempts.push({ prompt, messages });
toolCalls = messages.filter((m): m is Extract<SDKMessage, { type: "tool_call" }> => m.type === "tool_call");
toolResults = messages.filter((m): m is Extract<SDKMessage, { type: "tool_result" }> => m.type === "tool_result");
if (toolCalls.length > 0) break;
}
if (toolCalls.length === 0) {
log("tool lifecycle best-effort test observed no tool_call on this agent/model; not failing", {
selectedAgentName,
agentId,
});
} else {
expect(toolResults.length).toBeGreaterThan(0);
const callIds = new Set(toolCalls.map((m) => m.toolCallId));
for (const result of toolResults) {
expect(callIds.has(result.toolCallId)).toBe(true);
}
}
await writeFixture("tool_lifecycle", {
selectedAgentName,
attempts: attempts.map((a) => ({
prompt: a.prompt,
messages: a.messages.map(summarizeMessage),
})),
});
},
TEST_TIMEOUT_MS,
);
});

View File

@@ -0,0 +1,66 @@
import { describe, expect, test } from "bun:test";
import { extractStreamTextDelta } from "../stream-events.js";
describe("extractStreamTextDelta", () => {
test("extracts assistant delta from content_block_delta shape", () => {
const out = extractStreamTextDelta({
type: "content_block_delta",
delta: { type: "text_delta", text: "hello" },
});
expect(out).toEqual({ kind: "assistant", text: "hello" });
});
test("extracts reasoning delta from content_block_delta shape", () => {
const out = extractStreamTextDelta({
type: "content_block_delta",
delta: { type: "reasoning_delta", reasoning: "think" },
});
expect(out).toEqual({ kind: "reasoning", text: "think" });
});
test("extracts reasoning from message_type chunk shape", () => {
const out = extractStreamTextDelta({
message_type: "reasoning_message",
reasoning: "step by step",
});
expect(out).toEqual({ kind: "reasoning", text: "step by step" });
});
test("extracts assistant text from assistant_message string content", () => {
const out = extractStreamTextDelta({
message_type: "assistant_message",
content: "final answer",
});
expect(out).toEqual({ kind: "assistant", text: "final answer" });
});
test("extracts assistant text from assistant_message content parts", () => {
const out = extractStreamTextDelta({
message_type: "assistant_message",
content: [
{ type: "text", text: "hello " },
{ type: "text", text: "world" },
],
});
expect(out).toEqual({ kind: "assistant", text: "hello world" });
});
test("returns null for non-text stream events", () => {
const out = extractStreamTextDelta({
message_type: "tool_call_message",
tool_call: { name: "Bash", arguments: "{}" },
});
expect(out).toBeNull();
});
test("returns null for unknown/empty shapes", () => {
expect(extractStreamTextDelta({ type: "content_block_stop" })).toBeNull();
expect(extractStreamTextDelta({})).toBeNull();
});
});

View File

@@ -471,14 +471,43 @@ export interface SDKResultMessage {
conversationId: string | null;
}
export interface SDKStreamEventDeltaPayload {
type: string;
index?: number;
delta?: { type?: string; text?: string; reasoning?: string };
content_block?: { type?: string; text?: string };
[key: string]: unknown;
}
export interface SDKStreamEventMessagePayload {
message_type: string;
id?: string;
otid?: string | null;
content?: unknown;
reasoning?: string;
name?: string;
tool_call?: unknown;
tool_calls?: unknown;
tool_call_id?: string;
tool_return?: string;
status?: string;
[key: string]: unknown;
}
export interface SDKUnknownStreamEventPayload {
type?: string;
message_type?: string;
[key: string]: unknown;
}
export type SDKStreamEventPayload =
| SDKStreamEventDeltaPayload
| SDKStreamEventMessagePayload
| SDKUnknownStreamEventPayload;
export interface SDKStreamEventMessage {
type: "stream_event";
event: {
type: string; // "content_block_start" | "content_block_delta" | "content_block_stop"
index?: number;
delta?: { type?: string; text?: string; reasoning?: string };
content_block?: { type?: string; text?: string };
};
event: SDKStreamEventPayload;
uuid: string;
}