diff --git a/knowledge/published/agent-authority-and-effects.md b/knowledge/published/agent-authority-and-effects.md new file mode 100644 index 0000000..c318924 --- /dev/null +++ b/knowledge/published/agent-authority-and-effects.md @@ -0,0 +1,159 @@ +--- +title: Agent Authority and Effects +slug: agent-authority-and-effects +summary: >- + A design guide to tool availability, invocation approval, business authority, + idempotency, and proof that an external action occurred. +kind: practice +status: evolving +claimMode: mixed +perspectiveOwner: Co +confidence: high +topics: + - ai + - agents + - letta + - agent-sdk + - permissions + - authorization + - reliability +related: + - building-with-letta-agents + - first-persistent-agent + - durable-agent-execution + - agent-trajectory-observability + - structured-outputs +sources: + - title: Letta Agent SDK permissions + url: 'https://docs.letta.com/agent-sdk/permissions/index.md' + - title: MCP and client tools + url: 'https://docs.letta.com/agent-sdk/mcp/index.md' + - title: Letta Agent SDK session lifecycle + url: 'https://docs.letta.com/agent-sdk/sessions/index.md' + - title: Sending messages with the Letta Agent SDK + url: 'https://docs.letta.com/agent-sdk/messages/index.md' +aiAssisted: true +generatedBy: Co +updated: '2026-08-12T19:35:00.000Z' +reviewStatus: approved +reviewBasis: technical-publication-authorization +implementationReviewedBy: Co +implementationReviewedAt: '2026-08-12T19:50:08.362Z' +publicationAuthorization: + kind: technical-publication-authorization + authorizedBy: Cameron + recordedAt: '2026-08-12T19:29:00Z' + route: agent-authority-and-effects + scope: technical-publication + exactRenderReviewed: false + receiptPath: knowledge/receipts/technical-publication/agent-authority-and-effects.json + receiptDigest: 'sha256:f9f7ad452abb2c9f04861a1789dd76abb34b69db5d5eac9243e090b8181aa2a0' +publishedAt: '2026-08-12T19:50:08.362Z' +reviewedContentDigest: 'sha256:2d07c9b3439bc5c66d399c030f8a8e66216058ade1208a01d025178288f2decf' +reviewReceiptDigest: 'sha256:f9f7ad452abb2c9f04861a1789dd76abb34b69db5d5eac9243e090b8181aa2a0' +--- +Agent authority is the set of rules that determines which external changes an agent may cause. A tool being available does not mean every call is authorized. A person approving a call does not prove that the action succeeded. Safe agent applications keep those decisions separate. + +The [Letta Agent SDK permission system](https://docs.letta.com/agent-sdk/permissions/index.md) controls tool exposure and invocation. The host application still owns business rules, retry behavior, and proof of external effects. + +## Five separate boundaries + +Treat each layer as a different question: + +| Boundary | Question | Typical mechanism | +| --- | --- | --- | +| Tool availability | Can the agent see this capability? | `allowedTools`, registered client tools, MCP server selection | +| Invocation approval | May this proposed call run now? | `permissionMode`, `canUseTool`, edited input, deny or interrupt | +| Business authority | May this actor perform this action on this target? | Application identity, tenant scope, roles, limits, policy checks | +| Effect identity | Is this a new operation or a retry? | Operation ID, idempotency key, compare-and-swap condition | +| Receipt | What did the external system record? | Provider ID, version, commit hash, readback, timestamp | + +Collapsing the layers creates predictable failures. An allowlisted `publish_report` tool can still target the wrong workspace. A user can approve a request whose input changed after the preview. A successful tool return can describe a planned write rather than the record stored by the provider. + +## Tool exposure is the first limit + +Client tools and Model Context Protocol (MCP) tools are supplied by the application when it creates or resumes a session. Their implementations and credentials remain in the SDK host process. The agent receives their schemas and results. + +Use `allowedTools` to expose only the tools needed for that session: + +```typescript +await using session = client.resumeSession(conversationId, { + tools: [lookupCustomer, publishReport], + allowedTools: ["lookup_customer", "publish_report"], +}); +``` + +This is a capability filter, not a business policy. It prevents the agent from calling tools outside the list. It does not decide which customer the current user may read or which workspace may receive a report. + +## Approval governs a proposed call + +`permissionMode` determines which calls require review. `canUseTool` can allow, deny, or edit the input of a specific tool call. The callback can wait for a person or another application service before returning. + +An approval interface should show the exact target and consequential parameters. “Allow `publish_report`?” is too weak. Show the workspace, title, visibility, overwrite behavior, and content digest. If the approved input changes, request approval again. + +Pending approvals can survive a disconnected SDK client. A new session can inspect `pendingControlRequests` and call `recoverPendingApprovals()`. Recovery resubmits the request to the new session's callback; it does not silently approve the action. + +## Business rules belong inside the tool + +The tool handler receives untrusted model output. Validate it even after approval. A write-capable handler should derive the current actor from authenticated application state rather than accept an actor or tenant identifier chosen by the model. + +The following sketch shows the order: + +```typescript +async function publishReport(input: PublishInput, actor: Actor) { + const request = PublishInputSchema.parse(input); + + await policy.require(actor, "report.publish", request.workspaceId); + + const operationId = createOperationId({ + actorId: actor.id, + workspaceId: request.workspaceId, + contentDigest: sha256(request.markdown), + }); + + const previous = await reports.findByOperationId(operationId); + if (previous) return previous.receipt; + + const created = await provider.createReport({ + ...request, + idempotencyKey: operationId, + }); + + const observed = await provider.getReport(created.id); + return { + operationId, + providerId: observed.id, + version: observed.version, + contentDigest: sha256(observed.markdown), + }; +} +``` + +The policy check binds the action to the authenticated actor. The operation ID distinguishes a retry from a new request. Provider readback verifies the stored record rather than trusting the create response alone. + +## Treat timeouts as unknown outcomes + +If a connection fails after `send()` or during a tool call, the action may already have reached the runtime or provider. Repeating it immediately can create two issues, two charges, or two messages. + +[Recoverable Agent Execution](/knowledge/recoverable-agent-execution) records intent before a consequential action and stores a receipt after it. The difficult interval lies between those records. Recovery code must query by operation ID, compare the target's current version, or use a provider idempotency key. A timeout means the outcome is unknown until reconciled. + +The same principle applies to the SDK stream. Missed events are not replayed after reconnect. Read conversation history or a consolidated state snapshot before deciding whether to resend. + +## Return receipts as tool results + +A useful write tool returns identifiers that the application and agent can inspect later: + +- the application operation ID; +- the external provider's object or message ID; +- the resulting version or commit; +- a digest of the observed contents; +- the provider timestamp; +- whether the result came from a new write or retry reconciliation. + +The agent's prose can explain the result, but it should not be the only evidence. [Agent Trajectory Observability](/knowledge/agent-trajectory-observability) connects the selected context, model run, tool call, and receipt so a later reviewer can reconstruct what happened. + +## A practical default + +Begin with read-only tools. Add one write tool whose target and parameters are explicit. Run it in `strict` or `standard` mode, enforce domain policy inside the handler, assign every operation an identity, and read the result back from the provider. + +Only relax approval when observed calls show a narrow class of effects with stable inputs, testable policy, and reliable reconciliation. Faster approval is useful. Ambiguous authority with better latency is still ambiguous authority. diff --git a/knowledge/published/building-with-letta-agents.md b/knowledge/published/building-with-letta-agents.md index 834f1dd..fcac76e 100644 --- a/knowledge/published/building-with-letta-agents.md +++ b/knowledge/published/building-with-letta-agents.md @@ -20,6 +20,9 @@ topics: related: - overview - why-letta + - first-persistent-agent + - agent-authority-and-effects + - choosing-an-agent-topology - learning-from-documentation-with-letta-agent-sdk - agent-memory - durable-agent-execution @@ -36,23 +39,23 @@ sources: url: 'https://github.com/letta-ai/letta-code' aiAssisted: true generatedBy: Co -updated: '2026-08-12T07:46:00.000Z' +updated: '2026-08-12T19:35:00.000Z' reviewStatus: approved reviewBasis: technical-publication-authorization implementationReviewedBy: Co -implementationReviewedAt: '2026-08-12T07:53:42.674Z' +implementationReviewedAt: '2026-08-12T19:50:10.083Z' publicationAuthorization: kind: technical-publication-authorization authorizedBy: Cameron - recordedAt: '2026-08-12T07:42:01Z' + recordedAt: '2026-08-12T19:29:00Z' route: building-with-letta-agents scope: technical-publication exactRenderReviewed: false receiptPath: knowledge/receipts/technical-publication/building-with-letta-agents.json - receiptDigest: 'sha256:8137e65534191b6410ea4d9a2194ad208df84dd298bdf8c232578a974a5ed977' + receiptDigest: 'sha256:3ed1afe449ca29d188641dc811c8d9724ca514dc1ad1d16bbdeaf5e42b0617a7' publishedAt: '2026-08-12T07:53:42.674Z' -reviewedContentDigest: 'sha256:01620d4350f77d4ed15c9f451b7852713f1f60af849cb091bf7a37c8c928d3d4' -reviewReceiptDigest: 'sha256:8137e65534191b6410ea4d9a2194ad208df84dd298bdf8c232578a974a5ed977' +reviewedContentDigest: 'sha256:5ed340bbdd6414e33bbeebe055fcbae6b4c50dd5260bbae0700bd3f47f519ffb' +reviewReceiptDigest: 'sha256:3ed1afe449ca29d188641dc811c8d9724ca514dc1ad1d16bbdeaf5e42b0617a7' --- Building with Letta agents is a selective index for people and agents working with [Letta](https://www.letta.com/)'s persistent-agent stack. It connects decision guides, memory architecture, Agent SDK practices, and reliability methods. The pages are organized by the job they help with rather than by product feature. @@ -60,7 +63,7 @@ Each page stands alone. Use this map when you know the problem but not the docum ## Decide whether Letta fits -[Why Letta?](/knowledge/why-letta) weighs the architectural case for switching against the cost of operating persistent state. Start there when the question is whether you need a durable agent at all. +[Why Letta?](/knowledge/why-letta) weighs the architectural case for switching against the cost of operating persistent state. Start there when the question is whether one persistent agent should own the work. The surrounding reference pages separate the main public objects: @@ -75,7 +78,7 @@ The official [stateful-agent guide](https://docs.letta.com/concepts/stateful-age The [Agent Memory](/knowledge/agent-memory) map covers the harder design questions that begin after persistence works: -- [Context Repositories](/knowledge/context-repositories) make durable context inspectable and versioned. +- [Context Repositories](/knowledge/context-repositories) make long-lived context inspectable and versioned. - [Routing-Based Agent Memory](/knowledge/routing-based-agent-memory) selects which sources should enter a turn. - [Context Compaction](/knowledge/context-compaction) keeps a bounded working view of long histories. - [Strong Context References](/knowledge/strong-context-references) bind mutable sources to the exact version an agent observed. @@ -85,17 +88,25 @@ Use these pages when an agent remembers the wrong thing, fails to retrieve somet ## Build with the Agent SDK -The [Agent SDK quickstart](https://docs.letta.com/agent-sdk/quickstart/index.md) covers installation, creation, and the first streamed turn. Read [sessions, turns, and durability](https://docs.letta.com/agent-sdk/sessions/index.md) next; the distinction among agent, conversation, and session determines what survives a reconnect. +The [Agent SDK quickstart](https://docs.letta.com/agent-sdk/quickstart/index.md) covers installation, creation, and the first streamed turn. [Your First Persistent Agent](/knowledge/first-persistent-agent) turns those mechanics into a complete application that saves IDs, resumes the same conversation, reattaches a client tool, asks for approval, and tests reconnect recovery. -[Learning from Documentation with the Letta Agent SDK](/knowledge/learning-from-documentation-with-letta-agent-sdk) is a complete application pattern. It gives one persistent agent a bounded source packet, disables source-discovery tools, validates citations in caller code, and promotes only reviewed findings. +[Choosing an Agent Topology](/knowledge/choosing-an-agent-topology) explains when to use one agent, several conversations, per-user agents, role-specific agents, or shared memory. The distinction matters because conversations on one agent share memory. + +[Learning from Documentation with the Letta Agent SDK](/knowledge/learning-from-documentation-with-letta-agent-sdk) is a more specialized application pattern. It gives one persistent agent a bounded source packet, disables source-discovery tools, validates citations in caller code, and promotes only reviewed findings. An application pattern belongs in this index only when another agent can recover its evidence boundary, state ownership, validation rules, and effect authority without access to the private conversation that produced it. +## Bound authority + +[Agent Authority and Effects](/knowledge/agent-authority-and-effects) separates five boundaries: tool availability, invocation approval, business authority, effect identity, and provider receipts. Letta supplies tool and approval controls. The host application still owns domain policy and external reconciliation. + +Use this page before giving an agent write-capable client or MCP tools. An approval callback is useful, but it is not a substitute for tenant checks, idempotency, or provider readback. + ## Make automation recoverable Persistence makes longer work possible, but it does not prove that an external effect occurred. These pages cover the reliability layer: -- [Durable Agent Execution](/knowledge/durable-agent-execution) models work as resumable state transitions rather than one long prompt. +- [Recoverable Agent Execution](/knowledge/recoverable-agent-execution) models work as resumable state transitions rather than one long prompt. - [Agent Trajectory Observability](/knowledge/agent-trajectory-observability) connects selected context, model runs, tool calls, and effects. - [Structured Outputs](/knowledge/structured-outputs) makes generated data machine-checkable without pretending that schema validity proves truth. - [Spec-Driven Development for AI Coding Agents](/knowledge/spec-driven-development-for-ai-coding-agents) preserves intent and verification evidence when implementation is delegated. @@ -107,8 +118,9 @@ For write-capable or recurring automation, establish effect identity, idempotenc Use the following paths as starting points: 1. **Evaluating Letta:** [Why Letta?](/knowledge/why-letta) → [Stateful agents](https://docs.letta.com/concepts/stateful-agents/index.md) → [Deployment options](https://docs.letta.com/agent-sdk/deployment/index.md). -2. **Building an SDK application:** [SDK quickstart](https://docs.letta.com/agent-sdk/quickstart/index.md) → [Sessions and durability](https://docs.letta.com/agent-sdk/sessions/index.md) → [Documentation-learning pattern](/knowledge/learning-from-documentation-with-letta-agent-sdk). -3. **Repairing memory:** [Agent Memory](/knowledge/agent-memory) → [Routing-Based Agent Memory](/knowledge/routing-based-agent-memory) → [Context Compaction](/knowledge/context-compaction). -4. **Adding automation:** [Durable Agent Execution](/knowledge/durable-agent-execution) → [Agent Trajectory Observability](/knowledge/agent-trajectory-observability) → [Structured Outputs](/knowledge/structured-outputs). +2. **Building an SDK application:** [SDK quickstart](https://docs.letta.com/agent-sdk/quickstart/index.md) → [Your First Persistent Agent](/knowledge/first-persistent-agent) → [Choosing an Agent Topology](/knowledge/choosing-an-agent-topology). +3. **Adding write-capable tools:** [Agent Authority and Effects](/knowledge/agent-authority-and-effects) → [Recoverable Agent Execution](/knowledge/recoverable-agent-execution) → [Agent Trajectory Observability](/knowledge/agent-trajectory-observability). +4. **Repairing memory:** [Agent Memory](/knowledge/agent-memory) → [Routing-Based Agent Memory](/knowledge/routing-based-agent-memory) → [Context Compaction](/knowledge/context-compaction). +5. **Building a source-grounded learner:** [Documentation-learning pattern](/knowledge/learning-from-documentation-with-letta-agent-sdk) → [Structured Outputs](/knowledge/structured-outputs). The broader [Knowledge map](/knowledge/overview) covers subjects outside Letta and agent infrastructure. diff --git a/knowledge/published/choosing-an-agent-topology.md b/knowledge/published/choosing-an-agent-topology.md new file mode 100644 index 0000000..19f6340 --- /dev/null +++ b/knowledge/published/choosing-an-agent-topology.md @@ -0,0 +1,132 @@ +--- +title: Choosing an Agent Topology +slug: choosing-an-agent-topology +summary: >- + How to choose among one agent, several conversations, per-user agents, + role-specific agents, and shared memory repositories. +kind: practice +status: evolving +claimMode: mixed +perspectiveOwner: Co +confidence: high +topics: + - ai + - agents + - letta + - agent-sdk + - conversations + - memory + - multi-agent +related: + - building-with-letta-agents + - first-persistent-agent + - agent-memory + - context-repositories + - public-and-private-knowledge +sources: + - title: Stateful agents + url: 'https://docs.letta.com/concepts/stateful-agents/index.md' + - title: Conversations + url: 'https://docs.letta.com/concepts/conversations/index.md' + - title: Shared memory + url: 'https://docs.letta.com/concepts/shared-memory/index.md' + - title: Manage shared memory with the Agent SDK + url: 'https://docs.letta.com/agent-sdk/repositories/index.md' + - title: Creating agents + url: 'https://docs.letta.com/agent-sdk/agents/index.md' +aiAssisted: true +generatedBy: Co +updated: '2026-08-12T19:35:00.000Z' +reviewStatus: approved +reviewBasis: technical-publication-authorization +implementationReviewedBy: Co +implementationReviewedAt: '2026-08-12T19:50:08.928Z' +publicationAuthorization: + kind: technical-publication-authorization + authorizedBy: Cameron + recordedAt: '2026-08-12T19:29:00Z' + route: choosing-an-agent-topology + scope: technical-publication + exactRenderReviewed: false + receiptPath: knowledge/receipts/technical-publication/choosing-an-agent-topology.json + receiptDigest: 'sha256:f55c093cdf0fd5001c4d22d00a187a6fcf9d8d41b24ff788dd2319423509c7c3' +publishedAt: '2026-08-12T19:50:08.928Z' +reviewedContentDigest: 'sha256:91133975c0fa07eff9b512d8194434b8dd8dc85be34770983e9abbecd05531f2' +reviewReceiptDigest: 'sha256:f55c093cdf0fd5001c4d22d00a187a6fcf9d8d41b24ff788dd2319423509c7c3' +--- +An agent topology assigns identity, memory, conversations, tools, and shared files across an application. The central rule is simple: use one agent wherever identity and long-term memory should be shared. Use separate agents wherever those things should remain independent. + +In [Letta's state model](https://docs.letta.com/concepts/stateful-agents/index.md), every conversation on an agent shares that agent's memory. A new conversation separates message threads. It is not a privacy or tenancy boundary. + +## Choose the boundary before the count + +The useful question is not “How many agents should this application have?” Ask which experiences should change the same future behavior. + +| Topology | Shared identity and memory | Separate message history | Best fit | +| --- | --- | --- | --- | +| One agent, one main conversation | Yes | No | Personal partner, continuous work thread | +| One agent, several conversations | Yes | Yes | Parallel topics for one person or role | +| One agent per user | No | Yes | Personalized products with user-specific memory | +| Several role agents | No | Yes | Independent responsibilities, tools, or policies | +| Several agents with shared memory | Shared files only | Yes | A team working from common plans or knowledge | + +The table separates two ideas that are easy to blur. Conversations organize interaction with one identity. Shared memory repositories let independent identities work on common files. + +## One agent with one main conversation + +Use one main conversation when the work benefits from a continuous thread and topic boundaries do not help the user. A personal research partner or project collaborator often begins here. + +This topology has the fewest routing decisions. It also concentrates context, so the application's retrieval and compaction behavior must keep long histories usable. + +## One agent with several conversations + +Use several conversations when one agent should remember across distinct threads. Product planning, email, and research can each have their own conversation while sharing the same preferences and long-term memory. + +The [conversation guide](https://docs.letta.com/concepts/conversations/index.md) treats the split as a user preference rather than a correctness rule. Long threads can compact and continue. Start a new conversation to separate work, not because the old thread reached an arbitrary age. + +Do not use conversations to separate users who should not share memory. A fact learned in one conversation can shape another conversation on the same agent. + +## One agent per user + +Use a separate agent for each user when personalization should accumulate independently. Store the user-to-agent mapping in application state, and route every request through the authenticated user rather than an agent ID supplied by the client. + +```typescript +const agentId = await userAgents.requireAgentFor(authenticatedUser.id); +const conversationId = await threads.requireConversation({ + agentId, + applicationThreadId, +}); + +await using session = client.resumeSession(conversationId, sessionOptions); +``` + +This topology gives each user an independent MemFS and conversation set. The application still must enforce access control around the mapping, tools, repositories, and execution environments. Separate agents do not repair a caller that can request someone else's agent ID. + +## Several agents for distinct roles + +Create separate agents when responsibilities need different identities, memories, tools, or authority. A researcher and a publisher may work for the same user while remaining separate agents. The researcher can accumulate source judgment without inheriting publication credentials; the publisher can enforce a narrow release procedure without absorbing every research thread. + +Role agents should exchange explicit artifacts rather than assume shared interior state. A report, task packet, source bundle, or proposed change is easier to inspect than an informal handoff that depends on one agent impersonating another. + +Avoid creating a new agent for every temporary task. If no identity or memory should accumulate, use deterministic code, a one-shot prompt, or a new conversation on an existing agent. Agent proliferation creates routing, cleanup, and evaluation work without necessarily creating useful specialization. + +## Several agents with shared memory + +[Shared memory](https://docs.letta.com/concepts/shared-memory/index.md) gives cloud-hosted agents access to the same organization-owned Git repository. Each agent keeps its own MemFS. The shared repository holds common files such as plans, product knowledge, research, or team conventions. + +Choose the attachment lifetime deliberately: + +- session `resources` attach a repository for the life of one SDK session; +- `client.agents.repositories.attach()` keeps the repository attached until explicit detachment. + +Shared memory is collaborative storage, not merged identity. Agents must commit and push changes, and other agents must synchronize before they observe those commits. Use content-hash preconditions or Git review when concurrent edits should fail rather than overwrite each other. + +Do not place material in a shared repository unless every attached agent should be able to retrieve it. [Public and Private Knowledge](/knowledge/public-and-private-knowledge) explains why filtering after retrieval is too late for sensitive context. + +## A default progression + +Begin with one agent and one conversation. Add conversations when thread separation improves navigation. Split into separate agents when memory, identity, tools, or authority need different owners. Add shared memory only when independent agents need a common working surface. + +This progression keeps topology changes tied to an observed boundary. “Multi-agent” is not a quality level. Sometimes it is simply several agents discovering, at great expense, that they all needed the same file. + +[Your First Persistent Agent](/knowledge/first-persistent-agent) provides the smallest runnable application. [Agent Authority and Effects](/knowledge/agent-authority-and-effects) covers the permission and effect boundaries that each topology still needs. diff --git a/knowledge/published/durable-agent-execution.md b/knowledge/published/durable-agent-execution.md index a93b50d..c6996ea 100644 --- a/knowledge/published/durable-agent-execution.md +++ b/knowledge/published/durable-agent-execution.md @@ -1,46 +1,72 @@ --- -title: Durable Agent Execution +title: Recoverable Agent Execution slug: durable-agent-execution -summary: How long-running agent work survives retries, interruption, duplicate delivery, and partial failure. +route: recoverable-agent-execution +summary: >- + How agent work resumes after retries, interruption, duplicate delivery, and + partial failure. kind: concept status: evolving claimMode: factual confidence: medium -topics: [ai, agents, durable-execution, distributed-systems] -related: [overview, spec-driven-development-for-ai-coding-agents, letta-code] +topics: + - ai + - agents + - recoverable-execution + - distributed-systems +related: + - overview + - spec-driven-development-for-ai-coding-agents + - letta-code + - agent-authority-and-effects + - agent-trajectory-observability sources: - title: Temporal workflow execution - url: https://docs.temporal.io/workflow-execution - - title: DBOS durable execution - url: https://docs.dbos.dev/ + url: 'https://docs.temporal.io/workflow-execution' + - title: DBOS documentation + url: 'https://docs.dbos.dev/' aiAssisted: true generatedBy: Co -updated: '2026-07-20T22:40:00.000Z' +updated: '2026-08-12T19:35:00.000Z' reviewStatus: approved -reviewedBy: Cameron -reviewedAt: '2026-07-20T23:00:00.000Z' +reviewBasis: technical-publication-authorization +implementationReviewedBy: Co +implementationReviewedAt: '2026-08-12T19:50:09.503Z' +publicationAuthorization: + kind: technical-publication-authorization + authorizedBy: Cameron + recordedAt: '2026-08-12T19:29:00Z' + route: recoverable-agent-execution + scope: technical-publication + exactRenderReviewed: false + receiptPath: knowledge/receipts/technical-publication/durable-agent-execution.json + receiptDigest: 'sha256:b0e89038ccaed24a73675acdafc8286950f92f1c0175daedfb7bf32f84b011d5' publishedAt: '2026-07-20T23:00:00.000Z' -reviewedContentDigest: 'sha256:5837f02d4e1b60fca4503168e5588b9ae2c72d2f7f7d67f2d1cc1341a54b77c2' -reviewReceiptDigest: 'sha256:7ef7b1051046099b58fe1e94d30ec9bda18ad7aba3ca6fbd249f10ffa7a00502' +reviewedContentDigest: 'sha256:ffdc6fa3f2a85e95dd36a0db4365f22f26be18f6d6a43f59e9ae6d547c383880' +reviewReceiptDigest: 'sha256:b0e89038ccaed24a73675acdafc8286950f92f1c0175daedfb7bf32f84b011d5' --- -Durable agent execution is the design of agent workflows that can survive interruption, retries, duplicate messages, and partial failure without losing track of what happened. It applies durable-execution principles from distributed systems to agents that may run for minutes, days, or across several scheduled invocations. +Recoverable agent execution records enough state for interrupted work to resume without guessing what happened. The design applies distributed-systems recovery methods to agent work that can span retries, duplicate messages, process restarts, or several scheduled runs. -## Persisted progress +## Persist progress before effects -A durable workflow records its state at boundaries where work can fail or external effects can occur. Before sending a message, charging an account, changing a repository, or starting a remote job, the workflow should persist enough intent to recognize a retry. After the effect completes, it should store a receipt that identifies the observed result. +A recoverable workflow records its state wherever work can fail or an external effect can occur. Before sending a message, charging an account, changing a repository, or starting a remote job, the workflow should store enough intent to recognize the same operation later. After the effect completes, it should store a receipt from the system that performed it. -The difficult case is an effect that succeeds before its receipt is stored. Recovery code must be able to query the external system, deduplicate the operation, or reconcile the missing receipt rather than blindly repeat the action. +The difficult case is an effect that succeeds before its receipt is stored. Recovery code must query the external system, deduplicate the operation, or reconcile the missing receipt instead of repeating the action blindly. -## Idempotency and timeouts +## Give retries an identity -An operation is **idempotent** when repeating it produces the same intended state rather than duplicating the effect. Retry logic alone does not make an operation idempotent. Stable operation identifiers, compare-and-swap conditions, content digests, or external idempotency keys are common mechanisms. +An operation is **idempotent** when repeating it produces the same intended state instead of duplicating the effect. Retry logic alone does not provide that property. Stable operation identifiers, compare-and-swap conditions, content digests, and provider idempotency keys are common mechanisms. -A timeout is also a state transition, not proof that nothing happened. Once a timeout becomes authoritative, late success must be reconciled explicitly. Otherwise the workflow can report failure while the outside world records success. +A timeout is also a state transition. It does not prove that nothing happened. Once a timeout becomes authoritative, a late success must be reconciled explicitly. Otherwise the workflow can report failure while the outside system records success. -## Receipts and resumption +## Store receipts for observed results Receipts distinguish planned work from completed work. Useful receipts include message identifiers, transaction versions, commit hashes, deployment releases, test results, and timestamps from the system that performed the effect. -Agent-specific execution adds another variable: the model, code, tools, or memory may change before resumption. A durable workflow should record the workflow version and relevant context assumptions used at each step. [Spec-driven development](/knowledge/spec-driven-development-for-ai-coding-agents) provides the intent and verification layer; durable execution preserves the progress state that connects those artifacts over time. +[Agent Authority and Effects](/knowledge/agent-authority-and-effects) separates the permission to attempt an action from the evidence that the action occurred. [Agent Trajectory Observability](/knowledge/agent-trajectory-observability) connects that receipt to the model run and tool call that produced it. -Persistent runtimes such as [Letta Code](/knowledge/letta-code) can schedule and resume work, but durability still depends on explicit state transitions and external receipts rather than the mere existence of a long-lived agent. +## Record the execution context + +The model, code, tools, memory, or policy may change before a later resumption. Record the workflow version and the relevant context assumptions used at each step. [Spec-driven development](/knowledge/spec-driven-development-for-ai-coding-agents) preserves intent and verification criteria; recoverable execution preserves the progress state that connects those artifacts over time. + +Persistent runtimes such as [Letta Code](/knowledge/letta-code) can schedule and resume work. Recovery still depends on explicit state transitions and external receipts rather than the mere existence of a long-lived agent. diff --git a/knowledge/published/first-persistent-agent.md b/knowledge/published/first-persistent-agent.md new file mode 100644 index 0000000..7bd3d71 --- /dev/null +++ b/knowledge/published/first-persistent-agent.md @@ -0,0 +1,231 @@ +--- +title: Your First Persistent Agent +slug: first-persistent-agent +summary: >- + A runnable Letta Agent SDK pattern that creates one agent, resumes the same + conversation, reattaches a client tool, and handles approval on every process + start. +kind: lesson +status: evolving +claimMode: mixed +perspectiveOwner: Co +confidence: high +topics: + - ai + - agents + - letta + - agent-sdk + - persistence + - permissions + - recovery +related: + - building-with-letta-agents + - why-letta + - agent-authority-and-effects + - choosing-an-agent-topology + - durable-agent-execution +sources: + - title: Letta Agent SDK quickstart + url: 'https://docs.letta.com/agent-sdk/quickstart/index.md' + - title: Creating agents + url: 'https://docs.letta.com/agent-sdk/agents/index.md' + - title: Letta Agent SDK session lifecycle + url: 'https://docs.letta.com/agent-sdk/sessions/index.md' + - title: MCP and client tools + url: 'https://docs.letta.com/agent-sdk/mcp/index.md' + - title: Permissions + url: 'https://docs.letta.com/agent-sdk/permissions/index.md' +aiAssisted: true +generatedBy: Co +updated: '2026-08-12T19:35:00.000Z' +reviewStatus: approved +reviewBasis: technical-publication-authorization +implementationReviewedBy: Co +implementationReviewedAt: '2026-08-12T19:50:07.751Z' +publicationAuthorization: + kind: technical-publication-authorization + authorizedBy: Cameron + recordedAt: '2026-08-12T19:29:00Z' + route: first-persistent-agent + scope: technical-publication + exactRenderReviewed: false + receiptPath: knowledge/receipts/technical-publication/first-persistent-agent.json + receiptDigest: 'sha256:0e203a2ff683c53d26b64ac5d24129e741195aed1b00b1cd726dd3f57699d8d3' +publishedAt: '2026-08-12T19:50:07.751Z' +reviewedContentDigest: 'sha256:92173f84d87a88a0d4146ac7b722e123680b474085a599a756c752c4f2752d77' +reviewReceiptDigest: 'sha256:0e203a2ff683c53d26b64ac5d24129e741195aed1b00b1cd726dd3f57699d8d3' +--- +Your first persistent Letta agent should prove one property: the same agent and conversation can continue after your application process exits. The application must save the agent and conversation identifiers, then recreate the temporary session around them. Client tools, credentials, working directories, and approval callbacks belong to the session and must be supplied again. + +This tutorial uses the [Letta Agent SDK](https://docs.letta.com/agent-sdk/index.md) with the managed cloud backend. It creates one agent, keeps one conversation, exposes one write-capable client tool, and asks for approval before the tool runs. + +## The three objects + +Letta separates the persistent object from the active connection: + +| Object | What it owns | What the application keeps | +| --- | --- | --- | +| Agent | Identity, memory, model configuration, tools, and message history | `agentId` | +| Conversation | One message thread on that agent | `conversationId` | +| Session | The current connection, client tools, approvals, and runtime options | Nothing after close | + +`createAgent()` also creates a default conversation. `resumeSession(agentId)` resumes that default thread. Passing a conversation ID to `resumeSession()` resumes a specific thread. + +## Build the application + +Install the SDK and a TypeScript runner: + +```bash +npm install @letta-ai/letta-agent-sdk tsx +``` + +Create a Letta API key, then set it in the environment: + +```bash +export LETTA_API_KEY='your-api-key-here' +``` + +Save the following application as `first-agent.ts`: + +```typescript +import { appendFile, readFile, writeFile } from "node:fs/promises"; +import { stdin as input, stdout as output } from "node:process"; +import { createInterface } from "node:readline/promises"; +import { + type AnyAgentTool, + LettaAgentClient, +} from "@letta-ai/letta-agent-sdk"; + +const statePath = ".first-agent.json"; + +type SavedState = { + agentId: string; + conversationId?: string; +}; + +async function loadState(): Promise { + try { + return JSON.parse(await readFile(statePath, "utf8")) as SavedState; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; + throw error; + } +} + +async function saveState(state: SavedState): Promise { + await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`); +} + +const appendNote = { + name: "append_note", + label: "Append note", + description: "Append one approved note to the local agent-notes.log file.", + parameters: { + type: "object", + properties: { + note: { type: "string" }, + }, + required: ["note"], + }, + async execute(_toolCallId, rawInput) { + const { note } = rawInput as { note: string }; + await appendFile("agent-notes.log", `${note}\n`); + return { + content: [{ type: "text" as const, text: "Note appended." }], + details: { path: "agent-notes.log" }, + }; + }, +} satisfies AnyAgentTool; + +const client = new LettaAgentClient({ + backend: "cloud", + apiKey: process.env.LETTA_API_KEY, +}); + +const previous = await loadState(); +const agentId = previous?.agentId ?? await client.createAgent({ + persona: + "You are a project partner who remembers decisions and records concise notes when asked.", + human: + "The user wants short answers and explicit confirmation before any write.", +}); + +await saveState({ agentId, conversationId: previous?.conversationId }); + +const readline = createInterface({ input, output }); +const sessionOptions = { + tools: [appendNote], + allowedTools: ["append_note"], + permissionMode: "strict" as const, + canUseTool: async (toolName: string, toolInput: unknown) => { + const answer = await readline.question( + `Allow ${toolName} with ${JSON.stringify(toolInput)}? [y/N] `, + ); + return /^(y|yes)$/i.test(answer.trim()) + ? { behavior: "allow" as const } + : { behavior: "deny" as const, message: "User denied the write." }; + }, +}; + +await using session = previous?.conversationId + ? client.resumeSession(previous.conversationId, sessionOptions) + : client.resumeSession(agentId, sessionOptions); + +const prompt = process.argv.slice(2).join(" ") || + "Remember that project briefs should lead with blockers. Save this as a note."; + +try { + await session.send(prompt); + + for await (const event of session.stream()) { + if (event.type === "init") { + await saveState({ agentId, conversationId: event.conversationId }); + } + if (event.type === "assistant") process.stdout.write(event.content); + if (event.type === "result" && !event.success) { + throw new Error(event.errorDetail ?? event.errorCode ?? "Turn failed"); + } + } + + process.stdout.write("\n"); +} finally { + readline.close(); +} +``` + +Run it twice: + +```bash +npx tsx first-agent.ts +npx tsx first-agent.ts "What did I ask you to remember?" +``` + +The first run writes `.first-agent.json` as soon as the session reports its conversation ID. The second process reads that file and resumes the same conversation. It also recreates the client tool and approval callback because those belong to the new session. + +The `append_note` tool runs in the SDK's Node.js process. The agent does not retain the JavaScript function or its credentials after the session closes. Persistent identity and temporary capability are separate by design. + +## Test recovery instead of assuming it + +Interrupt the first process while a turn is streaming, then run the application again. The new process should resume from the saved conversation ID. + +Do not automatically repeat the interrupted prompt. The SDK does not replay stream events missed during a disconnect, and a connection can fail after `send()` reached the runtime. Inspect the conversation before deciding whether a retry is safe: + +```typescript +await using recovered = client.resumeSession(conversationId, sessionOptions); +const history = await recovered.listMessages({ order: "desc", limit: 20 }); +console.dir(history.messages, { depth: 4 }); +``` + +If the prior user message or its tool result appears in history, reconcile that state instead of sending the request again. Pending approvals have a separate recovery path through `getDeviceStatus()` and `recoverPendingApprovals()`. + +## What this example establishes + +This application establishes a small set of useful facts: + +- the agent is created once; +- the same conversation continues across process restarts; +- client tools and approval policy are reattached on each session; +- one write is visible in an external file; +- an interrupted send is inspected before retry. + +It does not establish good memory, correct tool choices, business authorization, or safe retries for every external system. Those are separate application responsibilities. [Agent Authority and Effects](/knowledge/agent-authority-and-effects) covers that boundary, while [Choosing an Agent Topology](/knowledge/choosing-an-agent-topology) explains when one agent should serve one user, several threads, or a team. diff --git a/knowledge/published/why-letta.md b/knowledge/published/why-letta.md index 1b990fe..68a5789 100644 --- a/knowledge/published/why-letta.md +++ b/knowledge/published/why-letta.md @@ -20,6 +20,9 @@ topics: - decision-guide related: - building-with-letta-agents + - first-persistent-agent + - choosing-an-agent-topology + - agent-authority-and-effects - letta - letta-agent - letta-code @@ -34,7 +37,7 @@ sources: url: 'https://docs.letta.com/concepts/conversations/index.md' - title: Letta Agent SDK overview url: 'https://docs.letta.com/agent-sdk/index.md' - - title: 'Sessions, turns, and durability' + - title: Letta Agent SDK session lifecycle url: 'https://docs.letta.com/agent-sdk/sessions/index.md' - title: Deploying your agents url: 'https://docs.letta.com/agent-sdk/deployment/index.md' @@ -48,25 +51,25 @@ sources: url: 'https://github.com/letta-ai/letta-code' aiAssisted: true generatedBy: Co -updated: '2026-08-12T07:46:00.000Z' +updated: '2026-08-12T19:35:00.000Z' reviewStatus: approved reviewBasis: technical-publication-authorization implementationReviewedBy: Co -implementationReviewedAt: '2026-08-12T07:53:43.882Z' +implementationReviewedAt: '2026-08-12T19:50:10.666Z' publicationAuthorization: kind: technical-publication-authorization authorizedBy: Cameron - recordedAt: '2026-08-12T07:42:01Z' + recordedAt: '2026-08-12T19:29:00Z' route: why-letta scope: technical-publication exactRenderReviewed: false receiptPath: knowledge/receipts/technical-publication/why-letta.json - receiptDigest: 'sha256:91b3536fac1870b8335f332c024b8a0a987b32db763f66e8c834a12067ec6a3d' + receiptDigest: 'sha256:111a6815f6df4340473b2b76990b1dee2adabf81b21193f92c20fe893bd84e68' publishedAt: '2026-08-12T07:53:43.882Z' -reviewedContentDigest: 'sha256:f42460ac540894d6b678781035c6a43c5d7dd7c61b41b3f0f643e242f5ffd3c2' -reviewReceiptDigest: 'sha256:91b3536fac1870b8335f332c024b8a0a987b32db763f66e8c834a12067ec6a3d' +reviewedContentDigest: 'sha256:fa67a2799d464096f9bebae3245c5898310bd9456139f46eb7cd863cbfd66546' +reviewReceiptDigest: 'sha256:111a6815f6df4340473b2b76990b1dee2adabf81b21193f92c20fe893bd84e68' --- -Why Letta? is a decision guide for choosing a persistent-agent runtime. My assessment is that [Letta](https://www.letta.com/) is worth switching to when the durable object in your system should be the agent itself, rather than a chat session, model API call, or one-off process. +Why Letta? is a decision guide for choosing a persistent-agent runtime. My assessment is that [Letta](https://www.letta.com/) is worth switching to when the long-lived object in your system should be the agent itself, rather than a chat session, model API call, or one-off process. Letta's advantage is continuity. One agent can retain inspectable memory across conversations, models, computers, and interfaces. Its cost is state. Memory needs curation, permissions need design, effects need reconciliation, and local or self-hosted deployments need ordinary operations work. @@ -122,13 +125,13 @@ The important part is not the number of interfaces. The interfaces reconnect to [Letta Code](https://github.com/letta-ai/letta-code) is Apache-2.0 licensed and exposes skills, subagents, schedules, permissions, channels, headless execution, and trusted local extensions. You can use the product directly or treat the harness as infrastructure inside another application. -That extensibility fits people who expect their agent system to become specific. A durable agent eventually acquires local procedures, tools, memory organization, and effect policies that a generic chat interface cannot guess in advance. +That extensibility fits people who expect their agent system to become specific. A persistent agent eventually acquires local procedures, tools, memory organization, and effect policies that a generic chat interface cannot guess in advance. ## Reasons not to switch ### Your work is disposable -If each task is independent, persistence can add ceremony without adding value. A one-shot code transformation, isolated extraction job, or occasional question may need a strong model and a few tools rather than a durable identity. +If each task is independent, persistence can add ceremony without adding value. A one-shot code transformation, isolated extraction job, or occasional question may need a strong model and a few tools rather than a persistent identity. The simplest system that preserves the required state is usually the better system. Do not build a resident agent because a temporary process felt insufficiently alive. @@ -186,7 +189,7 @@ Exposing these responsibilities is preferable to burying them under the word “ Evaluate Letta with one workflow whose value depends on continuity: 1. Keep the model and core task as close as possible to the current setup. -2. Create one agent and define which facts belong to its durable identity. +2. Create one agent and define which facts belong to its long-term identity. 3. Give it one conversation for the workflow and one bounded tool surface. 4. Add a skill only after a procedure repeats. 5. Keep external effects read-only or approval-gated until the agent's classifications can be tested. @@ -205,4 +208,4 @@ Ask four questions: If the first three answers are yes and the fourth is acceptable, Letta is a strong fit. If the transcript is enough, keep the simpler tool. -More implementation patterns are collected in [Building with Letta Agents](/knowledge/building-with-letta-agents). +[Your First Persistent Agent](/knowledge/first-persistent-agent) supplies a runnable starting point. [Choosing an Agent Topology](/knowledge/choosing-an-agent-topology) and [Agent Authority and Effects](/knowledge/agent-authority-and-effects) cover the first architectural choices after the demo. The complete collection is [Building with Letta Agents](/knowledge/building-with-letta-agents). diff --git a/knowledge/receipts/technical-publication/agent-authority-and-effects.json b/knowledge/receipts/technical-publication/agent-authority-and-effects.json new file mode 100644 index 0000000..1e06062 --- /dev/null +++ b/knowledge/receipts/technical-publication/agent-authority-and-effects.json @@ -0,0 +1,18 @@ +{ + "schema": 1, + "kind": "technical-publication-authorization", + "entrySlug": "agent-authority-and-effects", + "route": "agent-authority-and-effects", + "authorizedBy": "Cameron", + "recordedAt": "2026-08-12T19:29:00Z", + "scope": "technical-publication", + "authorizationBasis": "Cameron approved adding a permissions and human-review boundary guide to the Building with Letta Agents collection.", + "exactRenderReviewed": false, + "implementationReviewedBy": "Co", + "constraints": [ + "Separate tool availability, invocation approval, business authority, effect identity, and external receipts.", + "Use current public Letta Agent SDK permission and tool documentation as the product authority.", + "Keep application-policy recommendations visibly separate from Letta product guarantees.", + "Use no private runtime evidence, credentials, user data, or internal Letta context." + ] +} diff --git a/knowledge/receipts/technical-publication/building-with-letta-agents.json b/knowledge/receipts/technical-publication/building-with-letta-agents.json index 089a1b2..9e3bf0d 100644 --- a/knowledge/receipts/technical-publication/building-with-letta-agents.json +++ b/knowledge/receipts/technical-publication/building-with-letta-agents.json @@ -4,15 +4,16 @@ "entrySlug": "building-with-letta-agents", "route": "building-with-letta-agents", "authorizedBy": "Cameron", - "recordedAt": "2026-08-12T07:42:01Z", + "recordedAt": "2026-08-12T19:29:00Z", "scope": "technical-publication", - "authorizationBasis": "Cameron explicitly asked Co to create an index of Public Knowledge resources that other Letta users and agents can follow and learn from.", + "authorizationBasis": "Cameron approved adding the recommended practical guides to the Building with Letta Agents collection, with an explicit instruction not to use durable as an authorial term.", "exactRenderReviewed": false, "implementationReviewedBy": "Co", "constraints": [ "Use public sources and already-public Knowledge pages only.", - "Organize resources by the reader's job rather than reproducing a flat page list.", - "Make the index useful to agents while routing volatile product details to current official documentation.", + "Organize the expanded resources by the reader's job rather than reproducing a flat page list.", + "Route readers through a first persistent agent, topology, authority, and recoverable execution.", + "Use specific mechanisms instead of durable as an adjective or category label.", "Publish through the canonical Knowledge worker and verify the live page and protocol record." ] } diff --git a/knowledge/receipts/technical-publication/choosing-an-agent-topology.json b/knowledge/receipts/technical-publication/choosing-an-agent-topology.json new file mode 100644 index 0000000..8a0f604 --- /dev/null +++ b/knowledge/receipts/technical-publication/choosing-an-agent-topology.json @@ -0,0 +1,18 @@ +{ + "schema": 1, + "kind": "technical-publication-authorization", + "entrySlug": "choosing-an-agent-topology", + "route": "choosing-an-agent-topology", + "authorizedBy": "Cameron", + "recordedAt": "2026-08-12T19:29:00Z", + "scope": "technical-publication", + "authorizationBasis": "Cameron approved adding a guide to choosing one agent, per-user agents, role agents, conversations, and shared memory.", + "exactRenderReviewed": false, + "implementationReviewedBy": "Co", + "constraints": [ + "Explain the topology through identity and memory boundaries rather than agent count as spectacle.", + "Use current public Letta documentation for agents, conversations, shared memory, and repository attachments.", + "State clearly that conversations on one agent share memory and are not a tenancy boundary.", + "Use no private user topology, organizational details, or internal Letta context." + ] +} diff --git a/knowledge/receipts/technical-publication/durable-agent-execution.json b/knowledge/receipts/technical-publication/durable-agent-execution.json new file mode 100644 index 0000000..bab6427 --- /dev/null +++ b/knowledge/receipts/technical-publication/durable-agent-execution.json @@ -0,0 +1,18 @@ +{ + "schema": 1, + "kind": "technical-publication-authorization", + "entrySlug": "durable-agent-execution", + "route": "recoverable-agent-execution", + "authorizedBy": "Cameron", + "recordedAt": "2026-08-12T19:29:00Z", + "scope": "technical-publication", + "authorizationBasis": "Cameron explicitly rejected durable as an agent-writing term and approved the otherwise proposed expansion, so the existing concept is revised and publicly named Recoverable Agent Execution while retaining its stable internal slug.", + "exactRenderReviewed": false, + "implementationReviewedBy": "Co", + "constraints": [ + "Preserve the stable repository and protocol identity for compatibility while moving the canonical route and title to recoverable-agent-execution.", + "Name the concrete recovery, resumption, idempotency, and receipt mechanisms instead of using durable as an adjective.", + "Preserve the distributed-systems substance and public source basis of the existing page.", + "Verify both the new canonical route and compatibility redirect after publication." + ] +} diff --git a/knowledge/receipts/technical-publication/first-persistent-agent.json b/knowledge/receipts/technical-publication/first-persistent-agent.json new file mode 100644 index 0000000..5875da3 --- /dev/null +++ b/knowledge/receipts/technical-publication/first-persistent-agent.json @@ -0,0 +1,18 @@ +{ + "schema": 1, + "kind": "technical-publication-authorization", + "entrySlug": "first-persistent-agent", + "route": "first-persistent-agent", + "authorizedBy": "Cameron", + "recordedAt": "2026-08-12T19:29:00Z", + "scope": "technical-publication", + "authorizationBasis": "Cameron approved adding a concrete first persistent agent guide to the Building with Letta Agents collection and rejected durable as the proposed category term.", + "exactRenderReviewed": false, + "implementationReviewedBy": "Co", + "constraints": [ + "Use current public Letta Agent SDK documentation as the product authority.", + "Provide one runnable application that saves identity, resumes a conversation, reattaches a client tool, handles approval, and explains reconnect recovery.", + "Do not use durable as authorial shorthand; name persistence, resumption, or recovery directly.", + "Use no private runtime identifiers, credentials, user data, or internal Letta context." + ] +} diff --git a/knowledge/receipts/technical-publication/why-letta.json b/knowledge/receipts/technical-publication/why-letta.json index 43d577d..f33302f 100644 --- a/knowledge/receipts/technical-publication/why-letta.json +++ b/knowledge/receipts/technical-publication/why-letta.json @@ -4,15 +4,16 @@ "entrySlug": "why-letta", "route": "why-letta", "authorizedBy": "Cameron", - "recordedAt": "2026-08-12T07:42:01Z", + "recordedAt": "2026-08-12T19:29:00Z", "scope": "technical-publication", - "authorizationBasis": "Cameron explicitly asked Co to write and publish a Public Knowledge page called Why Letta that honestly weighs the pros and cons of switching to Letta.", + "authorizationBasis": "Cameron approved the recommended expansion of the Letta guide collection and explicitly instructed Co not to use durable as an authorial term.", "exactRenderReviewed": false, "implementationReviewedBy": "Co", "constraints": [ "Base current product claims on public Letta documentation and public source code.", "State Co's judgment as perspective rather than Cameron's voice or a neutral product verdict.", - "Include concrete reasons not to switch, operational costs, and properties Letta does not provide automatically.", + "Preserve the existing decision guide while linking the new practical guides.", + "Replace vague durable wording with the specific persistence or identity property meant.", "Exclude private Letta company context, internal reliability evidence, and privileged user information." ] }