Something went wrong. Try again.
A local-first event pipeline for independent agents, built on Jazz.
Something went wrong. Try again.
28 kB · 701 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702import fs from "node:fs/promises";import path from "node:path";import { afterEach, describe, expect, test } from "vitest";import { loadAgentDeclarations } from "../src/agents/declarations.js";import { temporaryProject, testDeclarationEnvironment } from "./helpers.js";
const roots: string[] = [];
afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true })));});
describe("agent declarations", () => { test("binds conceptualization contracts to graph events and strict JSON mode", async () => { const source = await fs.readFile( path.join(process.cwd(), "agents", "conceptualizer.yaml"), "utf8", ); const prompt = await fs.readFile( path.join(process.cwd(), "prompts", "conceptualizer.md"), "utf8", ); const cases = [ { name: "wrong-event", declaration: source.replace( " - stream.thought.derived.concept.graph", " - stream.thought.derived.topics", ), message: "must be selected together", }, { name: "wrong-contract", declaration: source.replace( "stream.thought.output.conceptualization", "stream.thought.output.observation", ), message: "must be selected together", }, { name: "conversation-text", declaration: source.replace( " model: gpt-4.1-mini", " model: gpt-4.1-mini\n outputMode: conversation-text", ), message: "strict JSON", }, ];
for (const current of cases) { const project = await temporaryProject(`thoughtstream-declaration-${current.name}-`); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); await fs.writeFile(path.join(agents, "conceptualizer.yaml"), current.declaration); await fs.writeFile(path.join(prompts, "conceptualizer.md"), prompt); await expect(loadAgentDeclarations(agents, {})).rejects.toThrow(current.message); } });
test("resolves a Tinker capability tier to a concrete persisted model", async () => { const declarations = await loadAgentDeclarations(path.join(process.cwd(), "agents"), testDeclarationEnvironment); expect(declarations.find((declaration) => declaration.id === "bluesky-enrichment-observer")).toMatchObject({ enabled: true, mode: "pi", provider: "tinker", providerProfile: "tinker-default", modelTier: "reasoning-small", model: "thinkingmachines/Inkling-Small", tools: ["atproto.fetch-markdown", "web.download-image"], }); expect(declarations.find((declaration) => declaration.id === "telegram-conversation")).toMatchObject({ version: 22, enabled: false, mode: "pi", provider: "tinker", providerProfile: "tinker-default", model: "thinkingmachines/Inkling-Small", thinkingLevel: "medium", outputMode: "conversation-text", sourcePatterns: ["telegram:thoughtstream-bot-webhook"], acceptedPrivacy: ["sensitive"], outputEventType: "stream.thought.derived.message.observation", payloadFields: ["text"], tools: [], proposals: ["memory-change", "self-correction", "focus-declaration"], initialReplay: "now", contextStrategy: "telegram-conversation", maxEvents: 100, conversationCompaction: { mode: "consume", agentId: "telegram-conversation-compactor", }, contextDocumentMaxChars: 64_000, contextDocumentSubscriptions: [{ source: "filesystem:telegram-agent-context", paths: ["identity.md", "memory.md"], required: true, }], conversationHistoryAgentIds: ["telegram-conversation"], accounting: { onExhaustion: "defer", reservation: { inputTokens: 80_000, outputTokens: 3_000, costMicrousd: 50_000 }, }, retry: { initialDelayMs: 5_000, maxDelayMs: 300_000 }, }); expect(declarations.find((declaration) => declaration.id === "output-repair")).toMatchObject({ enabled: true, mode: "pi", role: "repair", provider: "tinker", providerProfile: "tinker-default", modelTier: "escalation", model: "fixture/escalation-model", tools: [], }); expect(declarations.find((declaration) => declaration.id === "telegram-conversation-compactor")).toMatchObject({ version: 4, enabled: false, role: "compactor", mode: "pi", provider: "tinker", providerProfile: "tinker-default", model: "thinkingmachines/Inkling-Small", thinkingLevel: "off", maxOutputTokens: 1_200, outputMode: "compaction-text", maxEvents: 300, contextStrategy: "telegram-compaction", conversationHistoryAgentIds: ["telegram-conversation"], conversationCompaction: { mode: "produce", targetAgentId: "telegram-conversation", triggerInputChars: 80_000, retainInputChars: 40_000, }, outputEventType: "stream.thought.derived.conversation.compaction", tools: [], proposals: [], }); expect(declarations.find((declaration) => declaration.id === "stream-agent-conversation")).toMatchObject({ version: 1, enabled: true, role: "standard", mode: "pi", provider: "tinker", providerProfile: "tinker-default", model: "thinkingmachines/Inkling-Small", thinkingLevel: "medium", outputMode: "conversation-text", contextStrategy: "agent-conversation", eventTypes: ["stream.thought.source.agent.message"], sourcePatterns: ["agent-message:co"], acceptedPrivacy: ["sensitive"], initialReplay: "now", outputEventType: "stream.thought.derived.agent.message", contextDocumentSubscriptions: [{ source: "filesystem:telegram-agent-context", paths: ["identity.md", "memory.md"], required: true, }], tools: [], proposals: [], externalActions: false, }); expect(declarations.find((declaration) => declaration.id === "conceptualizer")).toMatchObject({ version: 14, enabled: true, mode: "pi", provider: "openai-compatible", providerProfile: "openai-json-default", model: "gpt-4.1-mini", outputContract: { id: "stream.thought.output.conceptualization", version: 1, sha256: expect.stringMatching(/^[a-f0-9]{64}$/), }, outputEventType: "stream.thought.derived.concept.graph", contextStrategy: "atproto-batch", atprotoObjectContext: true, tools: [], }); expect(declarations.find((declaration) => declaration.id === "post-training-course-tutor")).toMatchObject({ version: 1, enabled: true, mode: "pi", provider: "tinker", providerProfile: "tinker-default", model: "thinkingmachines/Inkling-Small", thinkingLevel: "medium", contextStrategy: "single-event", maxEvents: 1, payloadFields: ["courseId", "courseRevision", "lessonId", "lessonTitle", "sectionId", "sectionTitle", "question", "lessonContext"], eventTypes: ["stream.thought.source.course.question"], sourcePatterns: ["web-course:post-training-model-factory"], acceptedPrivacy: ["sensitive"], initialReplay: "now", outputEventType: "stream.thought.derived.message.observation", tools: [], proposals: [], externalActions: false, }); expect(declarations.find((declaration) => declaration.id === "resident-letta-conversation")).toMatchObject({ version: 6, enabled: true, mode: "letta-agent-sdk", provider: "letta-cloud", sourcePatterns: ["telegram:thoughtstream-bot-webhook", "batch:stream-activity"], acceptedPrivacy: ["sensitive", "private", "public-source"], contextStrategy: "activity-batch", maxEvents: 1, payloadFields: ["text"], lettaAgent: { backend: "cloud", agentIdEnv: "THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID", conversation: "main", responseMode: "conversation-text", batchResponseMode: "strict-json", permissionMode: "unrestricted", dreaming: { trigger: "off" }, sandbox: { ttlMinutes: 5, terminateOnClose: false }, }, accounting: { reservation: { inputTokens: 80_000, outputTokens: 2_000 }, limits: [ expect.objectContaining({ window: "rolling", durationMs: 300_000, maxCalls: 100, maxInputTokens: 100_000_000, maxOutputTokens: 10_000_000, }), expect.objectContaining({ window: "hour", maxCalls: 1_000, maxInputTokens: 1_000_000_000, maxOutputTokens: 100_000_000, }), expect.objectContaining({ window: "day", maxCalls: 10_000, maxInputTokens: 1_000_000_000, maxOutputTokens: 1_000_000_000, }), ], }, }); expect(declarations.find((declaration) => declaration.id === "cameron-bluesky-listener")).toMatchObject({ version: 2, enabled: false, mode: "letta-agent-sdk", provider: "letta-cloud", model: "chatgpt-plus-pro/gpt-5.6-luna", sourcePatterns: ["batch:cameron-atproto"], acceptedPrivacy: ["public-source"], privacyFloor: "private", contextStrategy: "atproto-batch", atprotoObjectContext: true, lettaAgent: { agentIdEnv: "THOUGHTSTREAM_LETTA_BLUESKY_LISTENER_AGENT_ID", responseMode: "strict-json", outputOnly: true, permissionMode: "strict", skillSources: [], }, tools: [], externalActions: false, }); expect(declarations.find((declaration) => declaration.id === "cameron-x-listener")).toMatchObject({ version: 1, enabled: false, model: "chatgpt-plus-pro/gpt-5.6-luna", sourcePatterns: ["batch:cameron-x-activity"], acceptedPrivacy: ["sensitive", "private", "public-source"], contextStrategy: "activity-batch", lettaAgent: { agentIdEnv: "THOUGHTSTREAM_LETTA_X_LISTENER_AGENT_ID", responseMode: "strict-json", outputOnly: true, permissionMode: "strict", skillSources: [], }, }); expect(declarations.find((declaration) => declaration.id === "cameron-social-listener")).toMatchObject({ version: 1, enabled: false, model: "chatgpt-plus-pro/gpt-5.6-terra", sourcePatterns: ["batch:cameron-social-observations"], contextStrategy: "activity-batch", requireCompletedAgentOutputs: true, lettaAgent: { agentIdEnv: "THOUGHTSTREAM_LETTA_SOCIAL_LISTENER_AGENT_ID", responseMode: "strict-json", outputOnly: true, permissionMode: "strict", skillSources: [], }, }); 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) .toBe("agent-telegram-fixture"); });
test("resolves an enabled Letta Cloud agent from its trusted environment reference", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration, ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), );
const loaded = await loadAgentDeclarations(agents, { THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID: "agent-cloud-fixture", });
expect(loaded[0]).toMatchObject({ enabled: true, mode: "letta-agent-sdk", provider: "letta-cloud", lettaAgent: { agentId: "agent-cloud-fixture" }, }); });
test("activates each social listener only through its own environment gate and agent identity", async () => { const declarations = await loadAgentDeclarations(path.join(process.cwd(), "agents"), { ...testDeclarationEnvironment, THOUGHTSTREAM_LETTA_ENABLE_BLUESKY_LISTENER: "1", THOUGHTSTREAM_LETTA_BLUESKY_LISTENER_AGENT_ID: "agent-bluesky-listener-fixture", THOUGHTSTREAM_LETTA_ENABLE_X_LISTENER: "true", THOUGHTSTREAM_LETTA_X_LISTENER_AGENT_ID: "agent-x-listener-fixture", THOUGHTSTREAM_LETTA_ENABLE_SOCIAL_LISTENER: "1", THOUGHTSTREAM_LETTA_SOCIAL_LISTENER_AGENT_ID: "agent-social-listener-fixture", }); expect(declarations.filter((declaration) => declaration.id.startsWith("cameron-")).map((declaration) => ({ id: declaration.id, enabled: declaration.enabled, agentId: declaration.lettaAgent?.agentId, }))).toEqual([ { id: "cameron-bluesky-listener", enabled: true, agentId: "agent-bluesky-listener-fixture" }, { id: "cameron-social-listener", enabled: true, agentId: "agent-social-listener-fixture" }, { id: "cameron-x-listener", enabled: true, agentId: "agent-x-listener-fixture" }, ]); await expect(loadAgentDeclarations(path.join(process.cwd(), "agents"), { ...testDeclarationEnvironment, THOUGHTSTREAM_LETTA_ENABLE_X_LISTENER: "1", })).rejects.toThrow("THOUGHTSTREAM_LETTA_X_LISTENER_AGENT_ID"); await expect(loadAgentDeclarations(path.join(process.cwd(), "agents"), { ...testDeclarationEnvironment, THOUGHTSTREAM_LETTA_ENABLE_BLUESKY_LISTENER: "1", THOUGHTSTREAM_LETTA_BLUESKY_LISTENER_AGENT_ID: "agent-shared-listener-fixture", THOUGHTSTREAM_LETTA_ENABLE_X_LISTENER: "1", THOUGHTSTREAM_LETTA_X_LISTENER_AGENT_ID: "agent-shared-listener-fixture", })).rejects.toThrow("distinct agent identities"); });
test("activates the disabled output-only local Coil consumer only through its explicit environment gate", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); await fs.copyFile( path.join(process.cwd(), "agents", "coil-public-knowledge.yaml"), path.join(agents, "coil-public-knowledge.yaml"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "coil-public-knowledge.md"), path.join(prompts, "coil-public-knowledge.md"), );
const [inactive] = await loadAgentDeclarations(agents, {}); expect(inactive?.enabled).toBe(false); expect(inactive?.lettaAgent?.agentId).toBeUndefined(); const [active] = await loadAgentDeclarations(agents, { THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE: "1", THOUGHTSTREAM_LETTA_CO_AGENT_ID: "agent-co-fixture", THOUGHTSTREAM_LETTA_CO_MEMORY_DIR: "/tmp/co-memory", }); expect(active).toMatchObject({ id: "coil-public-knowledge", version: 3, enabled: true, provider: "letta-local", contextStrategy: "coil-public-knowledge", lettaAgent: { backend: "local", agentId: "agent-co-fixture", conversation: "per-document", outputOnly: true, proposalTool: "public-knowledge-diff", permissionMode: "strict", skillSources: [], }, }); await expect(loadAgentDeclarations(agents, { THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE: "sometimes", })).rejects.toThrow("must be one of 1, true, 0, or false"); const declarationPath = path.join(agents, "coil-public-knowledge.yaml"); await fs.writeFile( declarationPath, (await fs.readFile(declarationPath, "utf8")).replace(" replay: now", " replay: beginning"), ); await expect(loadAgentDeclarations(agents, { THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE: "1", THOUGHTSTREAM_LETTA_CO_AGENT_ID: "agent-co-fixture", THOUGHTSTREAM_LETTA_CO_MEMORY_DIR: "/tmp/co-memory", })).rejects.toThrow("replay now"); });
test("rejects a cost limit when the resident declaration omits a cost reservation", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), 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"), path.join(prompts, "resident-letta-conversation.md"), );
await expect(loadAgentDeclarations(agents, {})) .rejects.toThrow("Window cost limit requires a cost reservation"); });
test("requires token limits when cost accounting is omitted", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration .replace("enabled: true", "enabled: false") .replace(" maxOutputTokens: 100000000\n", ""), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), );
await expect(loadAgentDeclarations(agents, {})).rejects.toThrow("maxOutputTokens"); });
test("rejects Letta SDK declarations that replay synthetic history", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration .replace("strategy: activity-batch", "strategy: telegram-conversation") .replace("maxEvents: 1", "maxEvents: 8"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), );
await expect(loadAgentDeclarations(agents, { THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID: "agent-cloud-fixture", })).rejects.toThrow("Letta Agent SDK declarations require one trigger event context"); });
test("accepts bounded concrete resident sources and rejects wildcard namespaces", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration .replace("- telegram:thoughtstream-bot-webhook", "- telegram:*"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), );
await expect(loadAgentDeclarations(agents, { THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID: "agent-cloud-fixture", })).rejects.toThrow("Letta Agent SDK declarations require one to eight concrete source namespaces"); });
test("fails closed when a repair declaration selects an unmapped escalation tier", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile(path.join(process.cwd(), "agents", "output-repair.yaml"), "utf8"); await fs.writeFile( path.join(agents, "output-repair.yaml"), declaration, ); await fs.copyFile( path.join(process.cwd(), "prompts", "output-repair.md"), path.join(prompts, "output-repair.md"), );
await expect(loadAgentDeclarations(agents, {})) .rejects.toThrow("No concrete model mapping for tinker/escalation"); });
test("loads the production repair declaration from its trusted-host escalation mapping", async () => { const declarations = await loadAgentDeclarations(path.join(process.cwd(), "agents"), testDeclarationEnvironment); expect(declarations.find((declaration) => declaration.id === "output-repair")).toMatchObject({ enabled: true, role: "repair", modelTier: "escalation", model: "fixture/escalation-model", }); });
test("rejects declaration-controlled provider URLs and credential selectors", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); await fs.mkdir(agents); await fs.mkdir(path.join(project, "prompts")); await fs.writeFile(path.join(project, "prompts", "agent.md"), "Read only.\n"); await fs.writeFile(path.join(agents, "agent.yaml"), [ "id: unsafe-provider", "version: 1", "description: Must fail before execution", "enabled: true", "subscribe: { types: ['*'], sources: ['*'], privacy: [private] }", "context: { maxEvents: 1, maxChars: 1000 }", "runner:", " kind: pi", " profile: tinker-default", " model: Qwen/Qwen3.5-4B", " baseUrl: https://attacker.invalid/v1", " apiKeyEnv: ATTACKER_KEY", " maxOutputTokens: 100", " timeoutMs: 1000", "prompt: prompts/agent.md", "emit: [stream.thought.derived.document.read]", "policy: { tools: [], externalActions: false }", ].join("\n"));
await expect(loadAgentDeclarations(agents)).rejects.toThrow("Unrecognized keys"); });
test("requires a trusted provider profile for Pi declarations", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); await fs.mkdir(agents); await fs.mkdir(path.join(project, "prompts")); await fs.writeFile(path.join(project, "prompts", "agent.md"), "Read only.\n"); await fs.writeFile(path.join(agents, "agent.yaml"), [ "id: missing-profile", "version: 1", "description: Must fail before execution", "enabled: true", "subscribe: { types: ['*'], sources: ['*'], privacy: [private] }", "context: { maxEvents: 1, maxChars: 1000 }", "runner: { kind: pi, model: Qwen/Qwen3.5-4B, maxOutputTokens: 100, timeoutMs: 1000 }", "prompt: prompts/agent.md", "emit: [stream.thought.derived.document.read]", "policy: { tools: [], externalActions: false }", ].join("\n"));
await expect(loadAgentDeclarations(agents)).rejects.toThrow("Pi agents require a trusted provider profile"); });
test("rejects output types outside the registered allowlist", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); await fs.mkdir(agents); await fs.mkdir(path.join(project, "prompts")); await fs.writeFile(path.join(project, "prompts", "agent.md"), "Read only.\n"); await fs.writeFile(path.join(agents, "agent.yaml"), [ "id: invalid-output", "version: 1", "description: Must fail before execution", "enabled: true", "subscribe:", " types: ['*']", " sources: ['*']", " privacy: [private]", "context: { maxEvents: 1, maxChars: 1000 }", "runner: { kind: deterministic, maxOutputTokens: 100, timeoutMs: 1000 }", "prompt: prompts/agent.md", "emit: [stream.thought.derived.not-registered]", "policy: { tools: [], externalActions: false }", ].join("\n"));
await expect(loadAgentDeclarations(agents)).rejects.toThrow("unregistered event type"); });
test("rejects conversation-text output outside the scoped Telegram adapter", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents); await fs.mkdir(prompts); const declaration = await fs.readFile(path.join(process.cwd(), "agents", "telegram-conversation.yaml"), "utf8"); await fs.writeFile( path.join(agents, "invalid-conversation-text.yaml"), declaration.replace("strategy: telegram-conversation", "strategy: single-event"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "telegram-conversation.md"), path.join(prompts, "telegram-conversation.md"), );
await expect(loadAgentDeclarations(agents)).rejects.toThrow( "Conversation-text output requires a standard Pi Telegram or private agent conversation declaration with no read-only model tools", ); });
test("rejects escaping, over-budget, and non-Telegram document subscriptions", async () => { const source = await fs.readFile(path.join(process.cwd(), "agents", "telegram-conversation.yaml"), "utf8"); const prompt = await fs.readFile(path.join(process.cwd(), "prompts", "telegram-conversation.md"), "utf8"); const cases = [ { name: "escaping-path", declaration: source.replace(" - identity.md", " - ../identity.md"), message: "normalized relative POSIX paths", }, { name: "document-budget", declaration: source.replace(" maxChars: 64000", " maxChars: 160000"), message: "leave at least 1024 characters", }, { name: "wrong-strategy", declaration: source.replace(" strategy: telegram-conversation", " strategy: single-event"), message: "supported only for Telegram conversations", }, ]; for (const current of cases) { const project = await temporaryProject(`thoughtstream-document-subscription-${current.name}-`); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents); await fs.mkdir(prompts); await fs.writeFile(path.join(agents, "telegram-conversation.yaml"), current.declaration); await fs.writeFile(path.join(prompts, "telegram-conversation.md"), prompt); await expect(loadAgentDeclarations(agents)).rejects.toThrow(current.message); } });});