Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 0 additions & 85 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,85 +0,0 @@
Q: What do you call a programmer from Boston? A: "a coda"

@TODO: This README is being neglected in favour of working on the actual functionality, but will clean up soon.

## Coda: Multi-agentic generative coding harness


## SupportBot: Event-Sourced Knowledge Graph Agent
A deterministic support agent architecture that treats the conversation as an append-only event log and uses Global Workspace Theory to build a transient world model (Knowledge Graph) for every inference turn.

1. Architectural Philosophy
Unlike "stateless" chat completion loops, this agent operates on a Data Plane vs. Control Plane separation:

Data Plane (The Log): Every user message, tool execution, and system state change is recorded as a discrete AgentEvent.

Control Plane (The Brain): A reducer function (rebuildGraph) collapses the event log into a directed graph of known entities (Orders, Issues, Users) before the LLM is even called.

2. Core Components
rebuildGraph (The Reducer)
The heart of the system. It consumes AgentSession.events and outputs a WorldModel.

Entity Extraction: Automatically promotes regex-matched IDs (e.g., #999) from USER_UPDATE events into Graph Nodes.

State Reconciliation: Merges TOOL_RESULT data into existing nodes, changing their state from UNRESOLVED to RESOLVED or CONFLICT.

resolveProtocol (The Router)
A dynamic system prompt selector. Based on the current graph state (e.g., an UNRESOLVED_CONFLICT node), it swaps the system instructions from General Support to Conflict Resolution.

supportAgent (The Async Generator)
An AsyncGenerator<AgentStep> that streams the internal "thinking" process.

Append USER_UPDATE to the log.

Rebuild the Knowledge Graph.

Serialize the graph into the System Prompt.

Inference via generateText.

Tool Execution (Oracle calls) and subsequent log append.

3. Data Flow & Event Schema
Events are strictly typed to prevent "hallucinated history."

TypeScript

type AgentEvent =
| { type: 'USER_UPDATE'; payload: { text: string }; timestamp: number }
| { type: 'TOOL_RESULT'; payload: { toolId: string; result: any }; timestamp: number }
| { type: 'STATE_TRANSITION'; payload: { from: string; to: string } };
The Serialization Pattern
To prevent the LLM from ignoring graph data (the "Lost in the Middle" problem), the system prompt is constructed as a prioritized array:

Context Primacy: The KNOWLEDGE_GRAPH_STATE is injected at the top of the prompt.

Constraints: Explicit "Operational Directives" (e.g., "Do not ask for IDs found in the graph").

Logic: The 1500+ character Protocol instructions follow.

4. Operational Guardrails
Tool Hallucination Recovery
The agent handles "Lazy LLM" syndrome where models return a tool's index (e.g., "0") instead of its name ("entity-lookup").

Normalization: The agent checks call.toolName against an alias map.

Key Mapping: Automatically maps varied LLM output keys (order_id, id, entityId) to a stable internal schema.

Regex Fallback
If the LLM fails to trigger a native tool call but emits a JSON-like block in the text stream, the agent uses a Regex + Zod parser to catch the intent and execute the tool anyway.

5. Testing & Instrumentation
Tests are located in tests/unit/support-agent.test.ts.

Debugging the "Blindness" Bug
If the agent asks for an ID it should already know, check the GRAPH_CONTEXT_SENT log.

Empty Graph: Check the Reducer's regex patterns.

Ignored Graph: Check the Prompt Hierarchy. The Knowledge Graph must occupy the Primacy position in the system array.

Next Steps:

Implement PERSISTENCE_LAYER to move the event log from memory to Postgres/Redis.

Add MULTI_ENTITY_RESOLVER for sessions involving multiple orders.
121 changes: 121 additions & 0 deletions apps/tui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { render } from "@opentui/solid";
import { createSignal } from "solid-js";
import { For } from "solid-js";
import {
OpenFeature,
MultiProvider,
FirstSuccessfulStrategy,
} from "@openfeature/server-sdk";

import { ProtocolResolver } from "@sup/lib/protocol-resolver";
import { adapters } from "@sup/tools";
import { JsonFileProvider } from "@sup/infra/adapters/JsonFileProvider";

const AGENT = process.env.AGENT || "support";
const { agent, modelSpec } = await (async () => {
if (AGENT === "coding") {
const { codingAgent, codingAgentModelSpec } = await import("@sup/agents/coding-agent");
return { agent: codingAgent, modelSpec: codingAgentModelSpec };
}
const { supportAgent, supportAgentModelSpec } = await import("@sup/agents/support-agent");
return { agent: supportAgent, modelSpec: supportAgentModelSpec };
})();

/*
const providers = [];
if (process.env.POSTHOG_API_KEY) {
const { PostHogProvider } = await import("@tapico/node-openfeature-posthog");
const { PostHog } = await import("posthog-node");
const posthogClient = new PostHog(process.env.POSTHOG_API_KEY, {
host: process.env.POSTHOG_HOST || "https://app.posthog.com",
});
providers.push({ provider: new PostHogProvider({ posthogClient }) });
}
providers.push({ provider: new JsonFileProvider("../../config/flags.json") });
*/

const multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy());
await OpenFeature.setProviderAndWait(multiProvider);

const session: AgentSession = {
id: "tui-session-" + Date.now(),
events: [],
};

type Message = { role: "user" | "agent"; text: string };

function App() {
const [messages, setMessages] = createSignal<Message[]>([]);
const [streaming, setStreaming] = createSignal("");
const [activeTool, setActiveTool] = createSignal("");
const [busy, setBusy] = createSignal(false);

async function submit(text: string) {
if (!text.trim() || busy()) return;
setBusy(true);
setMessages((m) => [...m, { role: "user", text }]);

const generator = agent(text, session, {
resolver: ProtocolResolver,
tools: adapters,
});

for await (const step of generator) {
if (step.type === "text_delta" && step.delta) {
setStreaming((s) => s + step.delta);
} else if (step.type === "tool_call") {
setActiveTool(step.toolId);
} else if (step.type === "tool_result") {
setActiveTool("");
} else if (step.type === "final") {
setMessages((m) => [
...m,
{ role: "agent", text: step.text || streaming() },
]);
setStreaming("");
setActiveTool("");
}
}

setBusy(false);
}

return (
<box width="100%" height="100%" flexDirection="column">
<scrollbox grow={1} width="100%">
<For each={messages()}>
{(msg) => (
<text>
<span color={msg.role === "user" ? "cyan" : "green"}>
{msg.role === "user" ? "You" : "Agent"}:{" "}
</span>
{msg.text}
<br />
</text>
)}
</For>
{streaming() && (
<text>
<span color="green">Agent: </span>
{streaming()}
</text>
)}
{activeTool() && (
<text>
<span color="yellow">[{activeTool()}]</span>
</text>
)}
</scrollbox>
<box borderTop width="100%">
<input
placeholder={busy() ? "..." : "You: "}
disabled={busy()}
onSubmit={submit}
width="100%"
/>
</box>
</box>
);
}

render(() => <App />);
3 changes: 3 additions & 0 deletions apps/tui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const { agent, modelSpec } = await (async () => {
return { agent: supportAgent, modelSpec: supportAgentModelSpec };
})();

/*
const providers = [];
if (process.env.POSTHOG_API_KEY) {
const { PostHogProvider } = await import("@tapico/node-openfeature-posthog");
Expand All @@ -32,8 +33,10 @@ if (process.env.POSTHOG_API_KEY) {
}
providers.push({ provider: new JsonFileProvider("../../config/flags.json") });


const multiProvider = new MultiProvider(providers, new FirstSuccessfulStrategy());
await OpenFeature.setProviderAndWait(multiProvider);
*/

const session: AgentSession = {
id: "tui-session-" + Date.now(),
Expand Down
Loading
Loading