diff --git a/agents/resident-letta-conversation.yaml b/agents/resident-letta-conversation.yaml index 8975f6e..82affff 100644 --- a/agents/resident-letta-conversation.yaml +++ b/agents/resident-letta-conversation.yaml @@ -1,14 +1,14 @@ id: resident-letta-conversation -version: 3 +version: 4 name: Resident Letta conversation description: Route Cameron's private Telegram messages and selected public ATProto activity into one persistent Letta Cloud agent conversation. -enabled: false +enabled: true subscribe: types: - stream.thought.source.telegram.message - stream.thought.derived.event.batch sources: - - telegram:thoughtstream-bot + - telegram:thoughtstream-bot-webhook - batch:cameron-atproto privacy: - sensitive diff --git a/agents/telegram-conversation-compactor.yaml b/agents/telegram-conversation-compactor.yaml index 5eb6d0b..fd0b627 100644 --- a/agents/telegram-conversation-compactor.yaml +++ b/agents/telegram-conversation-compactor.yaml @@ -1,12 +1,12 @@ id: telegram-conversation-compactor -version: 3 +version: 4 name: Stream Compactor description: Produce recursive private continuity boundaries from frozen canonical Stream history prefixes. role: compactor outputContract: id: stream.thought.output.conversation-compaction version: 1 -enabled: true +enabled: false subscribe: types: - stream.thought.source.telegram.message diff --git a/agents/telegram-conversation.yaml b/agents/telegram-conversation.yaml index ccd86a0..fa127ce 100644 --- a/agents/telegram-conversation.yaml +++ b/agents/telegram-conversation.yaml @@ -1,8 +1,8 @@ id: telegram-conversation -version: 21 +version: 22 name: Stream description: Reply to Cameron from exact subscribed documents and delivered same-chat history. -enabled: true +enabled: false subscribe: types: - stream.thought.source.telegram.message diff --git a/spec/agents.md b/spec/agents.md index 1fb38d2..0cd75e8 100644 --- a/spec/agents.md +++ b/spec/agents.md @@ -34,12 +34,14 @@ A Pi runner selects one exact learned Tinker release through `adapter: { id, ver Every `pi` and `letta-agent-sdk` declaration also requires an `accounting` policy. It declares a conservative per-call reservation, a lease longer than the runner timeout, and one or more rolling/hour/day limits. Calls, input tokens, and output tokens are always reserved; micro-US-dollar cost reservation and limits are optional. A cost limit without a cost reservation is invalid because the runtime cannot enforce a dimension it did not reserve. A declaration that cannot admit one complete reservation in every tracked dimension is invalid. Deterministic consumers cannot declare inference accounting. -The Inkling-Small Telegram declaration reserves $0.05 per attempt because the OpenAI-compatible beta reports tokens but not provider cost. That estimate bounds one full 131,072-token Pi context plus the declared output at the current serverless list price, rather than assuming an ordinary short turn or a cache hit. Its 30-day rolling window admits at most $50 of those conservative reservations. Settlement replaces token estimates with reported usage but retains the $0.05 cost estimate, so the local dollar window is deliberately an upper-bound ledger rather than a provider invoice. +The retained Inkling-Small Telegram declaration reserves $0.05 per attempt because the OpenAI-compatible beta reports tokens but not provider cost. That estimate bounds one full 131,072-token Pi context plus the declared output at the current serverless list price, rather than assuming an ordinary short turn or a cache hit. Its 30-day rolling window admits at most $50 of those conservative reservations. Settlement replaces token estimates with reported usage but retains the $0.05 cost estimate, so the local dollar window is deliberately an upper-bound ledger rather than a provider invoice. The declaration and its reciprocal compactor are currently disabled; they remain a reversible implementation path, not the active direct Telegram owner. -A Pi declaration may select an explicit `runner.thinkingLevel`; omission preserves the `off` default. The trusted parent binds that value into the sandbox packet, and the worker passes it to Pi's agent state rather than imposing one process-wide setting. For Tinker/Qwen chat-template models, `off` serializes `chat_template_kwargs.enable_thinking: false`; every non-off level serializes `enable_thinking: true` while the model/provider decides the internal budget. The Telegram Stream declaration uses `medium`. This setting is declaration identity and therefore appears in the declaration fingerprint and run context, while hidden reasoning content remains excluded from durable traces. +A Pi declaration may select an explicit `runner.thinkingLevel`; omission preserves the `off` default. The trusted parent binds that value into the sandbox packet, and the worker passes it to Pi's agent state rather than imposing one process-wide setting. For Tinker/Qwen chat-template models, `off` serializes `chat_template_kwargs.enable_thinking: false`; every non-off level serializes `enable_thinking: true` while the model/provider decides the internal budget. The retained Pi Telegram declaration selects `medium`. This setting is declaration identity and therefore appears in the declaration fingerprint and run context, while hidden reasoning content remains excluded from durable traces. The resident keeps measured token accounting and a conservative full-conversation reservation of 60,000 input / 2,000 output tokens per call, rather than estimating from the current packet size. Its tracked limits are emergency-only circuit breakers: 100 calls / 100,000,000 input / 10,000,000 output per rolling five minutes; 1,000 calls / 1,000,000,000 input / 100,000,000 output per hour; and 10,000 calls / 1,000,000,000 input / 1,000,000,000 output per day. Dollar cost is absent. This accounting is operational telemetry plus a catastrophic-runaway guard, not a budget or thrift mechanism; ordinary or extreme post, like, and Semble activity should not approach the ceilings. +Exactly one execution path owns direct Telegram replies. The active path is `resident-letta-conversation@4`: one persistent Letta Cloud agent main conversation receives the current webhook event as a bounded single trigger and owns its prior interaction internally. The Pi `telegram-conversation@22` and reciprocal `telegram-conversation-compactor@4` are disabled together, so one source event cannot produce competing Letta and Tinker replies. Switching paths is a coordinated declaration, credential, consumer, and dispatcher deployment; changing a model string alone is not a cutover. + ## Subscription The declaration compiles directly into a Jazz event query and live subscription. It may constrain event type, source, privacy class, address, typed payload fields, and bounded batching rules. The consumer process is both subscriber and runner; there is no separate matching service or queue. diff --git a/spec/architecture.md b/spec/architecture.md index 316658e..ef29c1e 100644 --- a/spec/architecture.md +++ b/spec/architecture.md @@ -34,7 +34,7 @@ Ordinary declaration/source pairs have independent scheduler keys. Stateful Lett Context enrichment is a named boundary owned by the trusted parent, not ad hoc model choreography. A source-specific compiler may dereference public objects, but it must bound and label each view, preserve the canonical source event, and durably snapshot the exact packet before the resident turn. Snapshots are runtime data, not repository artifacts. -Conversation compaction is a separate model-backed consumer, not hidden behavior inside reconstruction or the resident runner. A reciprocal no-tool clone observes the same Telegram source, skips below its deterministic threshold, and emits a private recursive boundary over one retry-stable frozen prefix. The resident selector may replace exactly the boundary's covered prefix with that typed historical summary while retaining exact newer turns. Canonical events and reconstruction remain unchanged; forks or divergent boundary evidence fail closed. See `compaction.md`. +Conversation compaction is a separate model-backed consumer, not hidden behavior inside reconstruction or a runner. On the retained Pi Telegram path, a reciprocal no-tool clone observes the same source, skips below its deterministic threshold, and emits a private recursive boundary over one retry-stable frozen prefix. The Pi selector may replace exactly the boundary's covered prefix with that typed historical summary while retaining exact newer turns. The active Letta resident instead owns continuity in its persistent main conversation, so the Pi parent and compactor are disabled together. Canonical events and reconstruction remain unchanged; forks or divergent boundary evidence fail closed. See `compaction.md`. Batching is a separate deterministic consumer stage. A strict manifest declaration names its id/version/enabled state, exact input event types and source ids, output source/type, quiet window, maximum age, maximum item count, privacy rule, replay rule, and bounded poll interval. It has no hidden prompt state and invokes no model. Batch identity is derived from the declaration fingerprint plus ordered canonical member event ids; the payload contains ordered strong references and bounded source metadata, never arbitrary source bodies. Batch insertion and source-local filtered-consumer progress settle atomically. That high-water mark may cross nonmatching cursor/lifecycle rows but may not cross an eligible matching row absent from the batch; settlement proves the complete authoritative matching interval through the final named member. Quiet/max-age decisions use durable eligible matching-event timestamps and filtered progress after every restart; unrelated raw-source events never postpone quiet flush, and max-items forces backpressure flushes. Current production use preserves one public source/privacy class, while mixed/private generalization must choose the most-private class and may not declassify. diff --git a/spec/compaction.md b/spec/compaction.md index 32bcf38..77ad3e6 100644 --- a/spec/compaction.md +++ b/spec/compaction.md @@ -2,7 +2,7 @@ ## Operational status -Compactor versions 1 and 2 repeatedly timed out on the first natural frozen prefix and emitted no boundary. Version 3 adopts the same operating shape as Letta's local sliding-window compaction: trigger from actual active-context pressure, summarize only the oldest prefix, keep a coherent exact recent tail, and let the trusted parent wrap plain model text in the typed boundary event. Canonical reconstruction remains complete regardless of projection success. +Compactor versions 1 and 2 repeatedly timed out on the first natural frozen prefix and emitted no boundary. Version 3 adopted the same operating shape as Letta's local sliding-window compaction: trigger from actual active-context pressure, summarize only the oldest prefix, keep a coherent exact recent tail, and let the trusted parent wrap plain model text in the typed boundary event. That Pi/Tinker parent-compactor pair is currently retained but disabled while the persistent Letta resident owns direct Telegram continuity. Canonical reconstruction remains complete regardless of projection activation or success. This contract covers model-context compaction for persistent conversation agents. It does not delete, rewrite, or retain less event history. Storage-retention compaction remains out of scope. diff --git a/test/agent-proposals.test.ts b/test/agent-proposals.test.ts index 0afad07..44bd0a8 100644 --- a/test/agent-proposals.test.ts +++ b/test/agent-proposals.test.ts @@ -644,6 +644,7 @@ async function setupConversation(currentText = "Please remember that and correct const loadedDeclaration = (await loadAgentDeclarations(path.join(process.cwd(), "agents"), testDeclarationEnvironment)) .find((candidate) => candidate.id === "telegram-conversation")!; const declaration = structuredClone(loadedDeclaration); + declaration.enabled = true; delete declaration.conversationCompaction; const prior = (await store.appendEvent(telegramMessage("prior", "The prior user fact."))).event; const priorContext = await buildSubscribedTelegramConversationContextPacket(declaration, prior, store); diff --git a/test/declarations.test.ts b/test/declarations.test.ts index a704418..3b4941f 100644 --- a/test/declarations.test.ts +++ b/test/declarations.test.ts @@ -72,8 +72,8 @@ describe("agent declarations", () => { tools: ["atproto.fetch-markdown", "web.download-image"], }); expect(declarations.find((declaration) => declaration.id === "telegram-conversation")).toMatchObject({ - version: 21, - enabled: true, + version: 22, + enabled: false, mode: "pi", provider: "tinker", providerProfile: "tinker-default", @@ -117,8 +117,8 @@ describe("agent declarations", () => { tools: [], }); expect(declarations.find((declaration) => declaration.id === "telegram-conversation-compactor")).toMatchObject({ - version: 3, - enabled: true, + version: 4, + enabled: false, role: "compactor", mode: "pi", provider: "tinker", @@ -183,11 +183,11 @@ describe("agent declarations", () => { tools: [], }); expect(declarations.find((declaration) => declaration.id === "resident-letta-conversation")).toMatchObject({ - version: 3, - enabled: false, + version: 4, + enabled: true, mode: "letta-agent-sdk", provider: "letta-cloud", - sourcePatterns: ["telegram:thoughtstream-bot", "batch:cameron-atproto"], + sourcePatterns: ["telegram:thoughtstream-bot-webhook", "batch:cameron-atproto"], acceptedPrivacy: ["sensitive", "public-source"], contextStrategy: "atproto-batch", maxEvents: 1, @@ -227,8 +227,17 @@ describe("agent declarations", () => { ], }, }); + const directTelegramOwners = declarations.filter((declaration) => ( + declaration.enabled + && declaration.eventTypes.includes("stream.thought.source.telegram.message") + && declaration.sourcePatterns.includes("telegram:thoughtstream-bot-webhook") + && declaration.outputEventType === "stream.thought.derived.message.observation" + )); + expect(directTelegramOwners.map((declaration) => ({ id: declaration.id, mode: declaration.mode }))).toEqual([ + { id: "resident-letta-conversation", mode: "letta-agent-sdk" }, + ]); expect(declarations.find((declaration) => declaration.id === "resident-letta-conversation")?.lettaAgent?.agentId) - .toBeUndefined(); + .toBe("agent-telegram-fixture"); }); test("resolves an enabled Letta Cloud agent from its trusted environment reference", async () => { @@ -244,7 +253,7 @@ describe("agent declarations", () => { ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), - declaration.replace("enabled: false", "enabled: true"), + declaration, ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), @@ -331,7 +340,9 @@ describe("agent declarations", () => { ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), - declaration.replace(" maxOutputTokens: 100000000", " maxOutputTokens: 100000000\n maxCostMicrousd: 10000000"), + declaration + .replace("enabled: true", "enabled: false") + .replace(" maxOutputTokens: 100000000", " maxOutputTokens: 100000000\n maxCostMicrousd: 10000000"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), @@ -355,7 +366,9 @@ describe("agent declarations", () => { ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), - declaration.replace(" maxOutputTokens: 100000000\n", ""), + declaration + .replace("enabled: true", "enabled: false") + .replace(" maxOutputTokens: 100000000\n", ""), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), @@ -380,8 +393,7 @@ describe("agent declarations", () => { path.join(agents, "resident-letta-conversation.yaml"), declaration .replace("strategy: atproto-batch", "strategy: telegram-conversation") - .replace("maxEvents: 1", "maxEvents: 8") - .replace("enabled: false", "enabled: true"), + .replace("maxEvents: 1", "maxEvents: 8"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), @@ -407,8 +419,7 @@ describe("agent declarations", () => { await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration - .replace("- telegram:thoughtstream-bot", "- telegram:*") - .replace("enabled: false", "enabled: true"), + .replace("- telegram:thoughtstream-bot-webhook", "- telegram:*"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), diff --git a/test/helpers.ts b/test/helpers.ts index 0a4b422..51587e8 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -6,6 +6,7 @@ import { JazzThoughtStore } from "../src/jazz/store.js"; import type { InferenceBudgetPolicy } from "../src/store/types.js"; export const testDeclarationEnvironment: NodeJS.ProcessEnv = { + THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID: "agent-telegram-fixture", THOUGHTSTREAM_TINKER_ESCALATION_MODEL: "fixture/escalation-model", }; diff --git a/test/telegram-bot.test.ts b/test/telegram-bot.test.ts index d0cfd69..8904ea4 100644 --- a/test/telegram-bot.test.ts +++ b/test/telegram-bot.test.ts @@ -202,6 +202,7 @@ describe("TelegramBotConnector", () => { const loadedDeclaration = declarations.find((candidate) => candidate.id === "telegram-conversation"); const declaration = loadedDeclaration ? structuredClone(loadedDeclaration) : undefined; if (!declaration) throw new Error("Missing Telegram conversation declaration"); + declaration.enabled = true; declaration.initialReplay = "beginning"; declaration.sourcePatterns = [trigger.source]; delete declaration.contextDocumentMaxChars; @@ -384,6 +385,7 @@ describe("TelegramBotConnector", () => { const loadedDeclaration = declarations.find((candidate) => candidate.id === "telegram-conversation"); const declaration = loadedDeclaration ? structuredClone(loadedDeclaration) : undefined; if (!declaration) throw new Error("Missing Telegram conversation declaration"); + declaration.enabled = true; declaration.initialReplay = "beginning"; declaration.sourcePatterns = [trigger.source]; delete declaration.contextDocumentMaxChars; @@ -890,6 +892,7 @@ describe("TelegramChannelDispatcher", () => { const loadedDeclaration = declarations.find((candidate) => candidate.id === "telegram-conversation"); const declaration = loadedDeclaration ? structuredClone(loadedDeclaration) : undefined; if (!declaration) throw new Error("Missing Telegram conversation declaration"); + declaration.enabled = true; declaration.initialReplay = "beginning"; declaration.sourcePatterns = [trigger.source]; delete declaration.contextDocumentMaxChars;