agentic-assistant 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pooria Arab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,160 @@
1
- # Temporary Holding Version
1
+ # agentic-assistant
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
+ [![npm version](https://img.shields.io/npm/v/agentic-assistant)](https://www.npmjs.com/package/agentic-assistant)
4
+ [![MIT license](https://img.shields.io/badge/license-MIT-blue)](https://opensource.org/license/mit)
5
+
6
+ [Package](https://www.npmjs.com/package/agentic-assistant) · [Source](https://github.com/pooriaarab/agentic-assistant) · [Support](https://github.com/pooriaarab/agentic-assistant/issues)
7
+
8
+ An AI assistant that acts inside your apps with the user's own permissions.
9
+
10
+ > Status: version 0.1.0, the first release. The core loop, the tool sources, approvals, and the React panel work.
11
+ > The chat CLI and the MCP server are not built yet. The `agentic-assistant` command only prints help and version.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm install agentic-assistant
17
+ ```
18
+
19
+ You need Node.js 18 or newer. The React panel needs `react` 18 or newer.
20
+
21
+ To build from source:
22
+
23
+ ```sh
24
+ git clone https://github.com/pooriaarab/agentic-assistant && cd agentic-assistant
25
+ npm ci && npm run build
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```ts
31
+ import { AnthropicProvider, ToolRegistry, createPrincipal, runAssistant } from "agentic-assistant";
32
+
33
+ const registry = new ToolRegistry().register({
34
+ name: "notes_read",
35
+ description: "Read one note by id",
36
+ inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
37
+ scope: "notes:read",
38
+ risk: "safe", // "risky" and "irreversible" tools wait for a person
39
+ handler: async ({ id }) => ({ text: `note ${id}` }),
40
+ });
41
+
42
+ // The host decides the scopes. The assistant never widens them.
43
+ const principal = createPrincipal({
44
+ id: "agent-1",
45
+ kind: "agent_of_human",
46
+ parentId: "user-42",
47
+ scopes: ["notes:read"],
48
+ });
49
+
50
+ for await (const event of runAssistant({
51
+ // Reads ANTHROPIC_API_KEY. Model claude-sonnet-5-5. useDefaultEndpoint calls a US-hosted API: pass baseUrl for an EU one.
52
+ provider: new AnthropicProvider({ useDefaultEndpoint: true }),
53
+ registry,
54
+ principal,
55
+ messages: [{ role: "user", content: [{ type: "text", text: "Read note 7." }] }],
56
+ limits: { maxTurns: 6, maxTotalTokens: 50_000 },
57
+ })) {
58
+ if (event.type === "text_delta") process.stdout.write(event.text);
59
+ }
60
+ ```
61
+
62
+ ## Load tools from an MCP server
63
+
64
+ ```ts
65
+ import { connectMcpTools } from "agentic-assistant";
66
+
67
+ const mcp = await connectMcpTools({
68
+ transport: { type: "stdio", command: "npx", args: ["-y", "some-notes-mcp-server"] },
69
+ // The host decides the scope of each tool, by name. There is no default. Do not trust
70
+ // `tool.annotations`: the server writes them. A tool that is not in the list is skipped.
71
+ scope: (tool) => {
72
+ const scope = { notes_read: "notes:read", notes_write: "notes:write" }[tool.name];
73
+ if (scope === undefined) throw new Error("tool is not on the host's list");
74
+ return scope;
75
+ },
76
+ // The host also says how risky each tool is. There is no default.
77
+ risk: (tool) => (tool.name === "notes_read" ? "safe" : "risky"),
78
+ });
79
+ registry.registerAll(mcp.tools); // mcp.skipped lists tools that were not loaded, with the reason
80
+ // ...run the assistant, then:
81
+ await mcp.close();
82
+ ```
83
+
84
+ An HTTP server works the same way: `{ type: "http", url: "https://tools.example.eu/mcp", headers: { authorization: "Bearer ..." } }`.
85
+
86
+ ## Ask a person before a risky action
87
+
88
+ ```ts
89
+ import { QueueApprovalGate } from "agentic-assistant";
90
+
91
+ const approvals = new QueueApprovalGate();
92
+ for await (const event of runAssistant({ provider, registry, principal, messages, approvals })) {
93
+ if (event.type === "approval_request") {
94
+ showToPerson(event.request); // tool, risk, exact arguments
95
+ // when the person clicks, from any handler:
96
+ // approvals.approve(event.request.id, "user-42")
97
+ // approvals.reject(event.request.id, "user-42", "not now")
98
+ }
99
+ }
100
+ ```
101
+
102
+ An approval covers one request: the tool, the exact arguments, and the acting principal. The request carries a digest of those three. The decision must return the same digest, or the call is refused. The tool runs on a copy of the exact arguments the person saw, so a change to the original object cannot reach it. The agent cannot approve its own action. A gate that fails, a missed deadline (`approvalTimeoutMs`, default five minutes), or a wrong answer means the call is refused. To connect your own approval service, implement `ApprovalGate`: one method that returns a decision.
103
+
104
+ ## Add the side panel
105
+
106
+ ```tsx
107
+ import { AssistantPanel } from "agentic-assistant/react";
108
+ import "agentic-assistant/react.css";
109
+
110
+ <AssistantPanel
111
+ title="Notes assistant"
112
+ // Run the assistant where your app wants: in the page, or behind your own HTTP stream.
113
+ run={(messages, signal) => runAssistant({ provider, registry, principal, messages, approvals, signal })}
114
+ // Pass a person's choice to your approval gate. Your app knows who the person is.
115
+ decide={(id, approved, reason) => (approved ? approvals.approve(id, userId) : approvals.reject(id, userId, reason))}
116
+ />;
117
+ ```
118
+
119
+ The panel streams the answer, shows each tool call as a card, and shows an approval prompt with the exact arguments. Every message, card, and prompt that the assistant makes carries a visible "Machine-made" label. Tool output is shown as text and never as HTML. The panel does not hold a model, a key, or a gate. It needs `react` 18 or newer.
120
+
121
+ The look comes from plain CSS variables (`--aa-bg`, `--aa-fg`, `--aa-accent`, and others). Set them on `.aa-panel` or a parent. The sheet loads no font, image, or file from another host. A prompt that the run left open (after Stop or an error) is closed and marked expired. Closing the panel stops the run.
122
+
123
+ ## Features
124
+
125
+ - **Model port.** `ModelProvider` has two adapters. `AnthropicProvider` needs a `baseUrl`, or `useDefaultEndpoint: true` to call `api.anthropic.com`, which is a US-hosted endpoint. There is no silent default. `OpenAICompatibleProvider` calls any server that speaks the chat-completions stream, so you can use an EU-hosted model. Both adapters need an `https` URL, except for `localhost`. If you pass your own `client` to `AnthropicProvider`, you own its endpoint. The OpenAI-compatible adapter needs an API key for any other host and cuts a response at a size limit.
126
+ - **Scoped tools.** Each tool names the scope it needs, for example `notes:write`. Scopes use the same `<resource>:<action>` format as `agentic-identity`. The model sees only the tools inside the scopes. The loop refuses a call to any other tool.
127
+ - **Tool sources.** Load tools from an MCP server over stdio or HTTP, or write typed SDK tools with `defineTool`. The loader skips a tool with a bad name, schema, or scope. It keeps text output and leaves images and other content out. It cuts long output, long descriptions, and long text inside schemas. It skips a duplicate name, a schema that is too large, and a schema with a regular expression (`pattern`). It stops at a page limit, a load time, and a size limit on each HTTP response.
128
+ - **Input validation.** The loop checks each call against the tool's JSON Schema before the handler runs. Nothing is coerced. A bad call returns an error to the model and never reaches the tool.
129
+ - **Tool output is data.** Each result is quoted in `<tool_output>` tags. Every `<` in the output is escaped. Every request carries a notice that tool output is never an instruction. This lowers the risk of prompt injection. It does not remove it: keep tools narrow and scopes small.
130
+ - **Approvals.** Every tool has a required `risk`: `safe`, `risky`, or `irreversible`. The loop asks the `ApprovalGate` before a risky or irreversible call. With no gate, those tools are not offered and are refused. The text a person writes when they decline goes to the event, not to the model.
131
+ - **React panel.** `agentic-assistant/react` has `AssistantPanel` and `useAssistant`. It meets the WCAG 2.2 AA points that code can check: named controls, a live conversation log, keyboard use, focus that moves to an approval prompt and then to the message box, buttons that are at least 32 px, a visible focus ring, and a color palette checked for contrast.
132
+ - **Fixed principal.** The assistant always acts as `agent_of_human`. `createPrincipal` throws when the kind is missing or different.
133
+ - **Caps.** A max-turn cap, a total-token cap, and a cost cap. A cap that trips stops the run. No tool runs after that. A tool result is cut at `maxToolResultChars`. A request that is sure to pass the token cap is not sent. The cost cap also limits the output of each request. A turn with more than `maxToolCallsPerTurn` calls (default 16) runs none of them.
134
+ - **Stops that fail closed.** An expired or revoked principal stops the run. So does a revocation check that throws or returns anything but `false`. The loop checks the principal again at the start of each run. A provider error ends the run with `reason: "provider_error"` and keeps the transcript. Truncated or refused model output never runs its tool calls.
135
+ - **Machine-made labels.** Every tool call, result, and the end of each run carry an `actor` with `machineMade: true`.
136
+ - **Test provider.** `agentic-assistant/testing` exports `ScriptedProvider`. It replays a fixed script for E2E tests.
137
+
138
+ ## Limits
139
+
140
+ - The chat CLI and the MCP server are not built. The `agentic-assistant` command prints help and version only.
141
+ - The panel is tested in a simulated browser (jsdom). Contrast is checked from the CSS values. Run a screen reader and a real browser before you ship it to people who need it.
142
+ - The panel shows plain text, not Markdown.
143
+ - The gate trusts the name that `decidedBy` carries. Your gate must check who the person is. `QueueApprovalGate` does not.
144
+ - An MCP server is trusted for its own output only. The assistant does not sandbox the server process. Run stdio servers you trust.
145
+ - Token and cost caps need the provider to report usage. The OpenAI-compatible adapter throws when the server sends none.
146
+ - A cost is an estimate from the `pricing` you give. Cache discounts are not applied. The built-in Anthropic prices are from 2026-10-06. Check them.
147
+ - The token pre-check guesses three characters per token. It is an estimate.
148
+ - A tool error text goes to the `detail` field of the event. The model sees only `tool failed`.
149
+ - A model refusal stops the run. No automatic fallback model is set up.
150
+
151
+ ## Develop
152
+
153
+ ```sh
154
+ npm ci
155
+ npm run ci:local
156
+ ```
157
+
158
+ ## License
159
+
160
+ MIT.
package/SKILL.md ADDED
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: agentic-assistant
3
+ description: An AI assistant that acts inside your apps with the user's own permissions. Use it to embed a scoped, capped, streaming agent loop with a model port. Keywords: assistant,ai,agent,tools,mcp,sdk,side-panel,approvals,eu,model-port.
4
+ ---
5
+
6
+ # agentic-assistant
7
+
8
+ ## Use the SDK
9
+
10
+ ```ts
11
+ import { runAssistant, ToolRegistry, createPrincipal, AnthropicProvider } from "agentic-assistant";
12
+
13
+ const principal = createPrincipal({ id: "agent-1", kind: "agent_of_human", parentId: "user-42", scopes: ["notes:read"] });
14
+ for await (const event of runAssistant({ provider: new AnthropicProvider({ useDefaultEndpoint: true }), registry, principal, messages })) {
15
+ // run_start, turn_start, text_delta, tool_call, approval_request, approval_decision, tool_result, turn_end, done
16
+ }
17
+ ```
18
+
19
+ ## Load tools from an MCP server
20
+
21
+ ```ts
22
+ const mcp = await connectMcpTools({ transport: { type: "stdio", command: "node", args: ["server.js"] }, scope: "notes:read", risk: "safe" });
23
+ registry.registerAll(mcp.tools);
24
+ ```
25
+
26
+ ## Ask a person first
27
+
28
+ Give each tool a `risk`. Pass `approvals: new QueueApprovalGate()` to `runAssistant`. On an `approval_request` event, show the exact arguments. Then call `approve(id, personId)` or `reject(id, personId)`.
29
+
30
+ ## Show a panel
31
+
32
+ ```tsx
33
+ <AssistantPanel run={(messages, signal) => runAssistant({ ...options, messages, signal })} decide={(id, ok) => (ok ? gate.approve(id, userId) : gate.reject(id, userId))} />
34
+ ```
35
+
36
+ Import `agentic-assistant/react.css` too. Label every assistant part as machine-made.
37
+
38
+ ## Test without a model
39
+
40
+ Use `ScriptedProvider` from `agentic-assistant/testing`. Give it one scripted turn per model call.
41
+
42
+ ## Rules
43
+
44
+ - Pass only the scopes the person granted.
45
+ - A denied call stays denied.
46
+ - Never approve your own action.
47
+ - Read `done.reason` to learn why a run stopped.
@@ -0,0 +1,7 @@
1
+ // src/version.ts
2
+ import { createRequire } from "module";
3
+ var VERSION = true ? "0.1.0" : readPackageVersion();
4
+
5
+ export {
6
+ VERSION
7
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ declare function runCli(argv: string[], out?: (line: string) => void, err?: (line: string) => void): number;
3
+
4
+ export { runCli };
package/dist/cli.js ADDED
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ VERSION
4
+ } from "./chunk-4QE7E5UQ.js";
5
+
6
+ // src/cli.ts
7
+ import { realpathSync } from "fs";
8
+ import { pathToFileURL } from "url";
9
+ var HELP = [
10
+ `agentic-assistant ${VERSION}`,
11
+ "An AI assistant that acts inside your apps with the user's own permissions.",
12
+ "",
13
+ "Usage: agentic-assistant [--help] [--version]"
14
+ ].join("\n");
15
+ function runCli(argv, out = console.log, err = console.error) {
16
+ const [first] = argv;
17
+ if (first === void 0 || first === "--help" || first === "-h") {
18
+ out(HELP);
19
+ return 0;
20
+ }
21
+ if (first === "--version" || first === "-V") {
22
+ out(VERSION);
23
+ return 0;
24
+ }
25
+ err(`unknown command "${first}"`);
26
+ err('Run "agentic-assistant --help" for usage.');
27
+ return 2;
28
+ }
29
+ function isMainModule() {
30
+ const invoked = process.argv[1];
31
+ if (typeof invoked !== "string") return false;
32
+ try {
33
+ return import.meta.url === pathToFileURL(realpathSync(invoked)).href;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+ if (isMainModule()) {
39
+ const code = runCli(process.argv.slice(2));
40
+ if (code !== 0) process.exit(code);
41
+ }
42
+ export {
43
+ runCli
44
+ };
@@ -0,0 +1,114 @@
1
+ import { S as Scope, T as ToolRisk, a as ToolDefinition } from './loop-CDbbDwSs.js';
2
+ export { A as ACTIONS, b as Action, c as Actor, d as ApprovalDecision, e as ApprovalGate, f as ApprovalRequest, g as AssistantEvent, h as AssistantPrincipal, D as DATA_NOTICE, i as DeniedReason, j as DoneReason, P as PrincipalError, k as PrincipalInput, Q as QueueApprovalGate, R as RunLimits, l as RunOptions, m as RunTotals, n as TOOL_RISKS, o as ToolContext, p as ToolRegistry, q as actorOf, r as assertPrincipal, s as canonicalJson, t as createPrincipal, u as defineTool, v as digestCall, w as hasScope, x as parseScope, y as runAssistant, z as scopeCovers } from './loop-CDbbDwSs.js';
3
+ import Anthropic from '@anthropic-ai/sdk';
4
+ import { M as ModelProvider, P as Pricing, a as ModelRequest, b as ModelEvent } from './types-C47SyHH5.js';
5
+ export { C as ContentBlock, J as JsonSchema, c as Message, d as ProviderError, S as StopReason, T as ToolSpec, U as Usage } from './types-C47SyHH5.js';
6
+
7
+ declare const VERSION: string;
8
+
9
+ type McpTransportConfig = {
10
+ type: "stdio";
11
+ command: string;
12
+ args?: string[];
13
+ env?: Record<string, string>;
14
+ cwd?: string;
15
+ } | {
16
+ type: "http";
17
+ url: string;
18
+ headers?: Record<string, string>;
19
+ };
20
+ interface McpToolInfo {
21
+ name: string;
22
+ description?: string;
23
+ annotations?: {
24
+ readOnlyHint?: boolean;
25
+ destructiveHint?: boolean;
26
+ idempotentHint?: boolean;
27
+ openWorldHint?: boolean;
28
+ };
29
+ }
30
+ interface ConnectMcpOptions {
31
+ transport: McpTransportConfig;
32
+ /** The scope each tool needs. There is no default. A function can choose per tool. */
33
+ scope: Scope | ((tool: McpToolInfo) => Scope);
34
+ /** How risky each tool is. There is no default. A function can choose per tool. Do not trust the server's hints. */
35
+ risk: ToolRisk | ((tool: McpToolInfo) => ToolRisk);
36
+ /** Put before each tool name, to keep names from two servers apart. */
37
+ prefix?: string;
38
+ /** Most characters of tool output to keep. Default 32768. */
39
+ maxOutputChars?: number;
40
+ /** Most tools to load. Default 200. */
41
+ maxTools?: number;
42
+ /** Longest tool description to pass to the model. Default 1000. */
43
+ maxDescriptionChars?: number;
44
+ /** Time to wait for one tool call. Default 30000. */
45
+ callTimeoutMs?: number;
46
+ /** Time to connect and list every tool, all pages together. Default 30000. */
47
+ loadTimeoutMs?: number;
48
+ /** Most bytes to read from one HTTP response. Default 4000000. Stdio is limited by the SDK. */
49
+ maxResponseBytes?: number;
50
+ }
51
+ interface McpTools {
52
+ tools: ToolDefinition[];
53
+ /** Tools that were not loaded, with the reason. */
54
+ skipped: {
55
+ name: string;
56
+ reason: string;
57
+ }[];
58
+ close(): Promise<void>;
59
+ }
60
+ /**
61
+ * Connects to an MCP server and turns its tools into assistant tools. The host
62
+ * names the scope each tool needs. Output text is capped, and non-text content is
63
+ * left out. Tools with a bad name, schema, or scope are skipped, not loaded.
64
+ */
65
+ declare function connectMcpTools(options: ConnectMcpOptions): Promise<McpTools>;
66
+
67
+ declare const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5-5";
68
+ interface AnthropicProviderOptions {
69
+ /** Read from ANTHROPIC_API_KEY when absent. */
70
+ apiKey?: string;
71
+ /** Point at a gateway or an EU-hosted Anthropic endpoint. Required unless `useDefaultEndpoint` is true. */
72
+ baseUrl?: string;
73
+ /** Opt in to api.anthropic.com, a US-hosted endpoint. Without it, `baseUrl` is required. */
74
+ useDefaultEndpoint?: boolean;
75
+ model?: string;
76
+ pricing?: Pricing;
77
+ effort?: "low" | "medium" | "high" | "xhigh" | "max";
78
+ client?: Anthropic;
79
+ }
80
+ declare class AnthropicProvider implements ModelProvider {
81
+ readonly name = "anthropic";
82
+ readonly model: string;
83
+ readonly pricing?: Pricing;
84
+ private readonly client;
85
+ private readonly effort?;
86
+ constructor(options?: AnthropicProviderOptions);
87
+ stream(request: ModelRequest): AsyncGenerator<ModelEvent>;
88
+ }
89
+
90
+ interface OpenAICompatibleOptions {
91
+ /** Base URL that serves `/chat/completions`, for example an EU-hosted endpoint. Required. */
92
+ baseUrl: string;
93
+ model: string;
94
+ /** Required for any host except localhost. An empty key is refused. */
95
+ apiKey?: string;
96
+ pricing?: Pricing;
97
+ /** Most bytes to read from one response, comments and keep-alive lines included. Default 4,000,000. */
98
+ maxResponseBytes?: number;
99
+ /** Provider name in events and errors. Default "openai-compatible". */
100
+ name?: string;
101
+ }
102
+ /** Speaks the OpenAI chat-completions streaming wire format, so any compatible server works. */
103
+ declare class OpenAICompatibleProvider implements ModelProvider {
104
+ readonly name: string;
105
+ readonly model: string;
106
+ readonly pricing?: Pricing;
107
+ private readonly baseUrl;
108
+ private readonly apiKey?;
109
+ private readonly maxResponseBytes;
110
+ constructor(options: OpenAICompatibleOptions);
111
+ stream(request: ModelRequest): AsyncGenerator<ModelEvent>;
112
+ }
113
+
114
+ export { AnthropicProvider, type AnthropicProviderOptions, type ConnectMcpOptions, DEFAULT_ANTHROPIC_MODEL, type McpToolInfo, type McpTools, type McpTransportConfig, ModelEvent, ModelProvider, ModelRequest, type OpenAICompatibleOptions, OpenAICompatibleProvider, Pricing, Scope, ToolDefinition, ToolRisk, VERSION, connectMcpTools };