sideagent 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,176 @@
1
- # Temporary Holding Version
1
+ # sideagent
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Embed a chat agent in your app. You give it an OpenAPI spec and an OpenAI-compatible host and key. It gives you a secure agent endpoint and a React chat UI.
4
+
5
+ ```
6
+ npm i sideagent
7
+ ```
8
+
9
+ Docs: https://egig.github.io/sideagent/
10
+
11
+ ## Server (`sideagent/server`)
12
+
13
+ ```ts
14
+ import { createAgent, honoHandler } from "sideagent/server";
15
+
16
+ const agent = await createAgent({
17
+ spec: "https://api.example.com/openapi.json", // or an object / JSON string
18
+ baseUrl: "https://api.example.com", // pinned; the spec's `servers` is ignored
19
+ llm: { baseUrl: "https://api.openai.com/v1", apiKey: process.env.LLM_API_KEY!, model: "gpt-4o-mini" },
20
+ allow: ["listInvoices", "createInvoice", "tag:reports"], // default-deny allowlist
21
+ allowedOrigins: ["https://app.example.com"],
22
+ approvalSecret: process.env.APPROVAL_SECRET, // set on serverless / multi-instance
23
+ getAuth: (req) => ({ userId: "…", headers: { authorization: req.headers.get("authorization")! } }),
24
+ });
25
+
26
+ app.on(["POST", "OPTIONS"], "/agent", honoHandler(agent)); // also: nextRoute, expressHandler
27
+ console.log(await agent.doctor()); // what the agent can do + LLM check
28
+ ```
29
+
30
+ Secure by default: allowlisted operations only, the end user's own credentials forwarded, non-GET calls need
31
+ server-verified approval bound to the exact arguments, pinned `baseUrl` with SSRF guards, size/step/time limits,
32
+ origin checks, threads bound to the user, redacted audit events.
33
+
34
+ Per-operation hints go in the spec as `x-agent`: `{ approval, description, invalidates: ["tag"], responseFields: ["id"] }`.
35
+
36
+ ## Client (`sideagent`)
37
+
38
+ ```tsx
39
+ import { AgentChat } from "sideagent";
40
+
41
+ <AgentChat
42
+ endpoint="/agent"
43
+ mode="floating" // "floating" | "sidebar"; the user can switch
44
+ hooks={{ onInvalidate: (tag) => tag === "invoices" && refetchInvoices() }}
45
+ />
46
+ ```
47
+
48
+ ### Docked sidebar
49
+
50
+ Wrap your app in `AgentProvider` to dock the chat on the right, side by side with the app instead of over it. It takes the same props as `AgentChat`. The user drags the panel's left edge (or uses the arrow keys on it) to resize; the width is remembered. Outside `AgentProvider`, sidebar mode overlays the page.
51
+
52
+ ```tsx
53
+ import { AgentProvider } from "sideagent";
54
+
55
+ <AgentProvider endpoint="/agent" mode="sidebar" defaultOpen>
56
+ <App />
57
+ </AgentProvider>
58
+ ```
59
+
60
+ `AgentProvider` fills the viewport height (`100dvh`); override with the `className` or `style` props.
61
+
62
+ Anything under `AgentProvider` can call `useAgent()` to get `{ open, setOpen }`, e.g. an "Ask assistant" button in your toolbar.
63
+
64
+ ## Try the demo (no API key needed)
65
+
66
+ ```
67
+ pnpm install
68
+ pnpm demo # fake invoices API + agent + mock LLM on :8787, UI on http://localhost:5173
69
+ ```
70
+ The demo has a dark mode toggle and uses `AgentProvider`. Run `pnpm demo:api` alone for just the API.
71
+ Set `LLM_API_KEY` (and optionally `LLM_BASE_URL`, `LLM_MODEL`) to use a real model.
72
+
73
+ ## Development
74
+
75
+ ```
76
+ pnpm build # tsup -> dist/
77
+ pnpm test # vitest
78
+ pnpm typecheck
79
+ ```
80
+
81
+ ## Theming
82
+
83
+ Neutral by default (grayscale, host font, 10px radius). `theme="auto"` follows the OS and a host `.dark` / `data-theme="dark"` / `data-bs-theme="dark"`; pass `"light"` or `"dark"` to force one. Match your design system with CSS variables on `:root`:
84
+
85
+ ```css
86
+ :root {
87
+ --sideagent-accent: var(--primary); /* buttons */
88
+ --sideagent-accent-fg: var(--primary-foreground);
89
+ --sideagent-bg: var(--background);
90
+ --sideagent-fg: var(--foreground);
91
+ --sideagent-border: var(--border);
92
+ --sideagent-surface: var(--muted); /* user bubble, hovers */
93
+ --sideagent-muted: var(--muted-foreground);
94
+ --sideagent-radius: 10px;
95
+ --sideagent-font: inherit;
96
+ }
97
+ ```
98
+
99
+ ## Results and icons
100
+
101
+ Tool results render as readable views: arrays of objects become tables (up to 50 rows and 8 columns, numbers right-aligned, dates and booleans formatted), objects become labelled fields, errors and rejections get their own state. Raw JSON stays under "Details". Tool results are plain text, never HTML. Assistant replies render as Markdown (GFM); raw HTML is dropped. Override per operation:
102
+
103
+ ```tsx
104
+ <AgentChat resultRenderers={{ listInvoices: ({ result }) => <MyInvoiceTable rows={result as any[]} /> }} />
105
+ ```
106
+
107
+ Icons come from [Radix Icons](https://github.com/radix-ui/icons) (MIT).
108
+
109
+ ## Chat history
110
+
111
+ `AgentChat` has a History button (list, open, delete), reopens the last conversation after a refresh, and restores a pending approval. Messages live in your LangGraph `checkpointer`; the list lives in a `threadStore` (in-memory by default). Both must be persistent in production:
112
+
113
+ ```ts
114
+ createAgent({ …, checkpointer: new PostgresSaver(…), threadStore: myThreadStore }); // ThreadStore: list / has / touch / delete
115
+ ```
116
+ History is only ever shown from the user's own index, and tool results are rebuilt from the already-filtered copies.
117
+
118
+ ## Knowledge (markdown and URLs)
119
+
120
+ ```ts
121
+ createAgent({
122
+ …,
123
+ knowledge: {
124
+ embeddings: { model: "text-embedding-3-small" }, // OpenAI-compatible /embeddings; baseUrl/apiKey default to llm's
125
+ sources: [{ id: "guide", title: "Invoicing guide", content: markdownString }], // pass strings: no filesystem on the edge
126
+ urls: ["https://docs.example.com/payment-terms", { url: "https://…", title: "Shipping" }], // fetched by the server only
127
+ },
128
+ vectorStore, // optional: your own VectorStore (pgvector, Vectorize…). Default is in-memory.
129
+ });
130
+ ```
131
+
132
+ The agent gets a `search_knowledge` tool; answers show a "Sources" card (links only for your configured URLs). Passages reach the model as labelled untrusted data and never reach the browser. URLs come only from config, must be public http(s) (private ranges need `allowPrivateNetworks`, metadata addresses never), follow only same-host redirects, and are capped by `maxSourceBytes`. URL sources refresh in the background after `ttlMs` (default 1h) or on `agent.knowledge.refresh()`. A source that fails to load is skipped with a warning and shown in `doctor()`.
133
+
134
+ With the default in-memory store, every start re-embeds all sources (embedding cost). Use a persistent `vectorStore` for large knowledge.
135
+
136
+ ## Cloudflare Workers
137
+
138
+ `sideagent/server` runs on Workers. Use `nodejs_compat` (or just `nodejs_als`) in `wrangler.jsonc`. LangGraph's approval pause (`interrupt()`) needs `AsyncLocalStorage`, and the Workers build of LangGraph does not register it. sideagent registers it for you when the runtime has it. Without the flag, reads work and the first approval fails (startup logs a warning).
139
+
140
+ ```jsonc
141
+ { "compatibility_date": "2025-09-01", "compatibility_flags": ["nodejs_compat"] }
142
+ ```
143
+
144
+ **Export conditions.** `sideagent/server` resolves to the real build under `workerd` and `worker`, listed before `browser` (which stays a throwing stub for real browser bundles). Wrangler (esbuild) and `@cloudflare/vite-plugin` need no alias. Both also set `browser`, so the order in `exports` matters.
145
+
146
+ **Workers AI as the LLM.** `env.AI` is a binding, not an HTTP endpoint, so use the adapter from `sideagent/workers-ai` instead of `llm`:
147
+
148
+ ```ts
149
+ import { createAgent } from "sideagent/server";
150
+ import { workersAiChatModel, workersAiEmbeddings } from "sideagent/workers-ai";
151
+
152
+ export default {
153
+ async fetch(request: Request, env: Env) {
154
+ agent ??= await createAgent({
155
+ spec, baseUrl: env.API_URL, allow: ["listInvoices", "createInvoice"],
156
+ allowedOrigins: ["https://app.example.com"], approvalSecret: env.APPROVAL_SECRET,
157
+ getAuth,
158
+ model: workersAiChatModel(env.AI, "@cf/openai/gpt-oss-120b"),
159
+ knowledge: { sources }, // optional
160
+ embeddings: workersAiEmbeddings(env.AI, "@cf/baai/bge-m3"), // replaces knowledge.embeddings
161
+ });
162
+ return agent.handler(request);
163
+ },
164
+ };
165
+ let agent: Awaited<ReturnType<typeof createAgent>> | undefined;
166
+ ```
167
+
168
+ `model` and `llm` are mutually exclusive; so are the `embeddings` option and `knowledge.embeddings`. `model` can be any LangChain chat model that supports `bindTools()`. `doctor()` checks an injected model with one `invoke`, not an HTTP call. The adapter takes the binding structurally (no `@cloudflare/workers-types` needed) and reads gpt-oss Responses-style output, chat-style `choices[0].message` and `{ response }`.
169
+
170
+ A custom `checkpointer` (D1, Durable Object storage) is a `BaseCheckpointSaver` with four methods: `getTuple`, `list`, `put`, `putWrites`. sideagent also calls `deleteThread` when a user deletes a thread.
171
+
172
+ ## For AI agents
173
+
174
+ - Skill: `npx skills add egig/sideagent`, then ask your coding agent to "add an AI assistant to this app". See [Quickstart](https://egig.github.io/sideagent/quickstart).
175
+ - [`AGENTS.md`](AGENTS.md): commands, layout and rules for coding agents working on this repo.
176
+ - [`llms.txt`](https://egig.github.io/sideagent/llms.txt) and [`llms-full.txt`](https://egig.github.io/sideagent/llms-full.txt): the docs in LLM-friendly form.
@@ -0,0 +1,97 @@
1
+ import { AgentEvent, ChatRequest } from './protocol.js';
2
+ import * as react from 'react';
3
+ import { ReactNode, CSSProperties } from 'react';
4
+
5
+ interface OperationEvent {
6
+ operationId: string;
7
+ status: number;
8
+ args: unknown;
9
+ result: unknown;
10
+ }
11
+ /**
12
+ * UI hooks. They run your code after the server confirms something happened, e.g. refetch a
13
+ * list after `createInvoice`. The model can never trigger them or any other client action.
14
+ */
15
+ interface AgentHooks {
16
+ onTurnStart?: () => void;
17
+ onTurnEnd?: () => void;
18
+ onApprovalRequested?: (e: Extract<AgentEvent, {
19
+ type: "approval.request";
20
+ }>) => void;
21
+ onError?: (e: {
22
+ code: string;
23
+ message: string;
24
+ }) => void;
25
+ /** Fires for every finished operation, successful or not. */
26
+ onAnyOperation?: (e: OperationEvent) => void;
27
+ /** Fires for one operationId. */
28
+ onOperationComplete?: Record<string, (e: OperationEvent) => void>;
29
+ /** Fires per `x-agent: { invalidates: [...] }` tag after a successful operation. */
30
+ onInvalidate?: (tag: string, e: OperationEvent) => void;
31
+ }
32
+
33
+ type AgentChatMode = "floating" | "sidebar";
34
+ interface AgentChatProps {
35
+ /** URL of your sideagent endpoint, e.g. "/api/agent". */
36
+ endpoint: string;
37
+ /** Starting layout. The user can switch; their choice is remembered. */
38
+ mode?: AgentChatMode;
39
+ title?: string;
40
+ placeholder?: string;
41
+ hooks?: AgentHooks;
42
+ /**
43
+ * Extra info about the page (route, selected record). The model sees it as untrusted data;
44
+ * it never grants permissions.
45
+ */
46
+ context?: unknown | (() => unknown);
47
+ /** Use "include" when the endpoint is on another origin and relies on cookies. */
48
+ credentials?: RequestCredentials;
49
+ headers?: Record<string, string>;
50
+ defaultOpen?: boolean;
51
+ /** Custom result views per operationId. Anything not listed gets the built-in table / fields view. */
52
+ resultRenderers?: Record<string, (e: {
53
+ operationId: string;
54
+ args: unknown;
55
+ result: unknown;
56
+ }) => ReactNode>;
57
+ /** "auto" follows the OS and host `.dark` / `data-theme=dark`. Colors are overridable via --sideagent-* CSS variables. */
58
+ theme?: "auto" | "light" | "dark";
59
+ /** Set false to hide the layout switcher. */
60
+ allowModeSwitch?: boolean;
61
+ }
62
+ interface AgentControls {
63
+ open: boolean;
64
+ setOpen: (open: boolean) => void;
65
+ }
66
+ /** Open or close the assistant from anywhere under `AgentProvider`. */
67
+ declare function useAgent(): AgentControls;
68
+ /** Lays the app and a docked sidebar chat side by side, and shares open state via `useAgent`. Takes the same props as AgentChat. */
69
+ declare function AgentProvider({ children, className, style, ...chat }: AgentChatProps & {
70
+ children: ReactNode;
71
+ className?: string;
72
+ style?: CSSProperties;
73
+ }): react.JSX.Element;
74
+ declare function AgentChat(props: AgentChatProps): react.JSX.Element;
75
+
76
+ interface TransportOptions {
77
+ endpoint: string;
78
+ credentials?: RequestCredentials;
79
+ headers?: Record<string, string>;
80
+ signal?: AbortSignal;
81
+ }
82
+ /** POSTs a chat request and yields the server's SSE events. */
83
+ declare function streamChat(body: ChatRequest, opts: TransportOptions): AsyncGenerator<AgentEvent>;
84
+ /** POSTs a non-streaming request (history actions) and returns the JSON body. */
85
+ declare function callJson<T>(body: ChatRequest, opts: Omit<TransportOptions, "signal">): Promise<{
86
+ status: number;
87
+ data: T | null;
88
+ }>;
89
+
90
+ /** "createdAt" / "created_at" -> "Created at" */
91
+ declare function humanize(key: string): string;
92
+ /** Turns an API result into a readable view. Plain text only: nothing here renders HTML. */
93
+ declare function ResultView({ result }: {
94
+ result: unknown;
95
+ }): ReactNode;
96
+
97
+ export { AgentChat, type AgentChatMode, type AgentChatProps, type AgentControls, AgentEvent, type AgentHooks, AgentProvider, ChatRequest, type OperationEvent, ResultView, callJson, humanize, streamChat, useAgent };