agentic-assistant 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/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +261 -2
- package/SKILL.md +64 -0
- package/dist/chat-pBg4wRjQ.d.ts +120 -0
- package/dist/chunk-6UD2QQA4.js +82 -0
- package/dist/chunk-BDXAUNMR.js +698 -0
- package/dist/chunk-HYT732TN.js +36 -0
- package/dist/chunk-T4ZMDOB4.js +674 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +327 -0
- package/dist/index.d.ts +89 -0
- package/dist/index.js +77 -0
- package/dist/loop-42giKLVa.d.ts +285 -0
- package/dist/mcp-server-X5VTHZVM.js +11 -0
- package/dist/react.css +57 -0
- package/dist/react.d.ts +56 -0
- package/dist/react.js +327 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +6 -0
- package/dist/types-DGH8FS56.d.ts +84 -0
- package/dist/worker.d.ts +41 -0
- package/dist/worker.js +276 -0
- package/llms.txt +17 -0
- package/package.json +95 -4
package/dist/worker.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
VERSION,
|
|
3
|
+
createPrincipal,
|
|
4
|
+
hasScope,
|
|
5
|
+
parseScope,
|
|
6
|
+
runAssistant
|
|
7
|
+
} from "./chunk-T4ZMDOB4.js";
|
|
8
|
+
|
|
9
|
+
// src/api.ts
|
|
10
|
+
var MAX_BODY_BYTES = 65536;
|
|
11
|
+
var MAX_MESSAGE_CHARS = 1e4;
|
|
12
|
+
var MAX_REASON_CHARS = 500;
|
|
13
|
+
var DEFAULT_MAX_CONCURRENT_RUNS = 4;
|
|
14
|
+
var KINDS = ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
|
|
15
|
+
var APPROVER_SCOPE = "assistant:admin";
|
|
16
|
+
var json = (body, status = 200, headers = {}) => Response.json(body, { status, headers: { "cache-control": "no-store", ...headers } });
|
|
17
|
+
var fail = (status, error, headers = {}) => json({ error }, status, headers);
|
|
18
|
+
function toCaller(context) {
|
|
19
|
+
if (typeof context !== "object" || context === null) return void 0;
|
|
20
|
+
const { principalId, kind, chain, scopes } = context;
|
|
21
|
+
if (typeof principalId !== "string" || principalId === "" || !KINDS.includes(kind)) return void 0;
|
|
22
|
+
if (!Array.isArray(chain) || chain.length === 0 || chain[chain.length - 1]?.id !== principalId) return void 0;
|
|
23
|
+
if (chain[chain.length - 1]?.kind !== kind || kind === "human" && chain.length !== 1) return void 0;
|
|
24
|
+
const ids = chain.map((step) => step?.id);
|
|
25
|
+
if (ids.some((id) => typeof id !== "string" || id === "")) return void 0;
|
|
26
|
+
const rootId = ids[0];
|
|
27
|
+
if (!Array.isArray(scopes)) return void 0;
|
|
28
|
+
try {
|
|
29
|
+
for (const scope of scopes) parseScope(scope);
|
|
30
|
+
} catch {
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
return { id: principalId, kind, rootId, chain: ids, scopes: [...scopes] };
|
|
34
|
+
}
|
|
35
|
+
var assistantId = (caller) => `assistant:${caller.id}`;
|
|
36
|
+
async function readJson(request) {
|
|
37
|
+
if (Number(request.headers.get("content-length") ?? 0) > MAX_BODY_BYTES) return void 0;
|
|
38
|
+
let text = "";
|
|
39
|
+
if (request.body !== null) {
|
|
40
|
+
const reader = request.body.getReader();
|
|
41
|
+
const decoder = new TextDecoder();
|
|
42
|
+
let bytes = 0;
|
|
43
|
+
for (; ; ) {
|
|
44
|
+
const chunk = await reader.read();
|
|
45
|
+
if (chunk.done) break;
|
|
46
|
+
bytes += chunk.value.byteLength;
|
|
47
|
+
if (bytes > MAX_BODY_BYTES) {
|
|
48
|
+
await reader.cancel();
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
52
|
+
}
|
|
53
|
+
text += decoder.decode();
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const body = JSON.parse(text);
|
|
57
|
+
return typeof body === "object" && body !== null && !Array.isArray(body) ? body : void 0;
|
|
58
|
+
} catch {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function sseChunk(event) {
|
|
63
|
+
return new TextEncoder().encode(`event: ${event.type}
|
|
64
|
+
data: ${JSON.stringify(event)}
|
|
65
|
+
|
|
66
|
+
`);
|
|
67
|
+
}
|
|
68
|
+
function createAssistantApi(options) {
|
|
69
|
+
const pending = /* @__PURE__ */ new Map();
|
|
70
|
+
const running = /* @__PURE__ */ new Map();
|
|
71
|
+
const maxRuns = options.maxConcurrentRuns ?? DEFAULT_MAX_CONCURRENT_RUNS;
|
|
72
|
+
function whyNotDecide(caller, entry) {
|
|
73
|
+
const { proposer } = entry;
|
|
74
|
+
if (caller.id === assistantId(proposer)) return "an agent cannot decide on its own action";
|
|
75
|
+
if (proposer.kind !== "human") {
|
|
76
|
+
if (caller.chain.includes(proposer.id) || caller.kind !== "human" && proposer.chain.includes(caller.id)) {
|
|
77
|
+
return "an agent cannot decide on its own action, or on one from an agent it made or that made it";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (caller.kind !== "human" && !hasScope(caller.scopes, APPROVER_SCOPE)) {
|
|
81
|
+
return `an agent needs the ${APPROVER_SCOPE} scope to decide`;
|
|
82
|
+
}
|
|
83
|
+
const tool = options.registry.get(entry.request.tool);
|
|
84
|
+
if (tool === void 0 || !hasScope(caller.scopes, tool.scope)) return "the approver does not hold the scope of this tool";
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
const gateFor = (proposer) => ({
|
|
88
|
+
request(request, signal) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
if (signal.aborted) return reject(new Error("the request was cancelled"));
|
|
91
|
+
pending.set(request.id, { request, proposer, resolve });
|
|
92
|
+
signal.addEventListener(
|
|
93
|
+
"abort",
|
|
94
|
+
() => {
|
|
95
|
+
pending.delete(request.id);
|
|
96
|
+
reject(new Error("the request was cancelled"));
|
|
97
|
+
},
|
|
98
|
+
{ once: true }
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
function chat(request, caller, message) {
|
|
104
|
+
const abort = new AbortController();
|
|
105
|
+
running.set(caller.id, (running.get(caller.id) ?? 0) + 1);
|
|
106
|
+
let released = false;
|
|
107
|
+
const release = () => {
|
|
108
|
+
if (released) return;
|
|
109
|
+
released = true;
|
|
110
|
+
const left = (running.get(caller.id) ?? 1) - 1;
|
|
111
|
+
if (left <= 0) running.delete(caller.id);
|
|
112
|
+
else running.set(caller.id, left);
|
|
113
|
+
};
|
|
114
|
+
request.signal.addEventListener(
|
|
115
|
+
"abort",
|
|
116
|
+
() => {
|
|
117
|
+
abort.abort();
|
|
118
|
+
release();
|
|
119
|
+
},
|
|
120
|
+
{ once: true }
|
|
121
|
+
);
|
|
122
|
+
let events;
|
|
123
|
+
try {
|
|
124
|
+
events = runAssistant({
|
|
125
|
+
provider: options.provider(),
|
|
126
|
+
registry: options.registry,
|
|
127
|
+
// The assistant holds the caller's scopes and no more.
|
|
128
|
+
principal: createPrincipal({
|
|
129
|
+
id: assistantId(caller),
|
|
130
|
+
kind: "agent_of_human",
|
|
131
|
+
parentId: caller.id,
|
|
132
|
+
scopes: caller.scopes,
|
|
133
|
+
// Each check asks the Auth again. A key revoked during a run, or one with other scopes, stops it.
|
|
134
|
+
isRevoked: async () => {
|
|
135
|
+
const now = toCaller(await options.auth.authenticate(request));
|
|
136
|
+
return now?.id !== caller.id || [...now.scopes].sort().join(" ") !== [...caller.scopes].sort().join(" ");
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
approvals: gateFor(caller),
|
|
140
|
+
messages: [{ role: "user", content: [{ type: "text", text: message }] }],
|
|
141
|
+
system: options.system,
|
|
142
|
+
limits: options.limits,
|
|
143
|
+
signal: abort.signal
|
|
144
|
+
});
|
|
145
|
+
} catch {
|
|
146
|
+
release();
|
|
147
|
+
return fail(500, "the assistant could not start");
|
|
148
|
+
}
|
|
149
|
+
const body = new ReadableStream({
|
|
150
|
+
async pull(controller) {
|
|
151
|
+
try {
|
|
152
|
+
const next = await events.next();
|
|
153
|
+
if (next.done) {
|
|
154
|
+
release();
|
|
155
|
+
controller.close();
|
|
156
|
+
} else controller.enqueue(sseChunk(next.value));
|
|
157
|
+
} catch {
|
|
158
|
+
release();
|
|
159
|
+
controller.enqueue(sseChunk({ type: "error", error: "the run failed" }));
|
|
160
|
+
controller.close();
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
cancel() {
|
|
164
|
+
abort.abort();
|
|
165
|
+
release();
|
|
166
|
+
void events.return(void 0).catch(() => void 0);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return new Response(body, {
|
|
170
|
+
headers: { "content-type": "text/event-stream", "cache-control": "no-store", "x-accel-buffering": "no" }
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async function decide(request, caller, id, approve) {
|
|
174
|
+
const body = await readJson(request);
|
|
175
|
+
if (body === void 0) return fail(400, "the body must be a JSON object");
|
|
176
|
+
const entry = pending.get(id);
|
|
177
|
+
if (entry === void 0 || entry.proposer.rootId !== caller.rootId) return fail(404, "no pending approval with this id");
|
|
178
|
+
const why = whyNotDecide(caller, entry);
|
|
179
|
+
if (why !== void 0) return fail(403, why);
|
|
180
|
+
let decision;
|
|
181
|
+
if (approve) {
|
|
182
|
+
if (body.digest !== entry.request.digest) return fail(409, "digest does not match the pending call");
|
|
183
|
+
decision = { requestId: id, approved: true, digest: entry.request.digest, decidedBy: caller.id, approverKind: caller.kind === "human" ? "human" : "agent" };
|
|
184
|
+
} else {
|
|
185
|
+
const reason = typeof body.reason === "string" ? body.reason.slice(0, MAX_REASON_CHARS) : void 0;
|
|
186
|
+
decision = { requestId: id, approved: false, decidedBy: caller.id, reason };
|
|
187
|
+
}
|
|
188
|
+
pending.delete(id);
|
|
189
|
+
entry.resolve(decision);
|
|
190
|
+
return json({ requestId: id, approved: approve, decidedBy: caller.id, machineMade: caller.kind !== "human" });
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
async fetch(request) {
|
|
194
|
+
const url = new URL(request.url);
|
|
195
|
+
const match = /^\/api\/v1\/(tools|chat|approvals)(?:\/([^/]+)\/(approve|deny))?\/?$/.exec(url.pathname);
|
|
196
|
+
if (match === null) return fail(404, "not found");
|
|
197
|
+
const [, route, id, verb] = match;
|
|
198
|
+
const allowed = route === "chat" || verb !== void 0 ? "POST" : "GET";
|
|
199
|
+
if (request.method !== allowed) return fail(405, "method not allowed", { allow: allowed });
|
|
200
|
+
if (route !== "approvals" && id !== void 0) return fail(404, "not found");
|
|
201
|
+
let caller;
|
|
202
|
+
try {
|
|
203
|
+
caller = toCaller(await options.auth.authenticate(request));
|
|
204
|
+
} catch {
|
|
205
|
+
caller = void 0;
|
|
206
|
+
}
|
|
207
|
+
if (caller === void 0) return fail(401, "sign in with a valid key", { "www-authenticate": "Bearer" });
|
|
208
|
+
if (route === "tools") {
|
|
209
|
+
const tools = options.registry.list().filter((tool) => hasScope(caller.scopes, tool.scope)).map(({ name, description, scope, risk, inputSchema }) => ({ name, description, scope, risk, inputSchema }));
|
|
210
|
+
return json({ tools });
|
|
211
|
+
}
|
|
212
|
+
if (route === "approvals" && verb === void 0) {
|
|
213
|
+
const list = [...pending.values()].filter((entry) => entry.proposer.rootId === caller.rootId).filter((entry) => {
|
|
214
|
+
const tool = options.registry.get(entry.request.tool);
|
|
215
|
+
return tool !== void 0 && hasScope(caller.scopes, tool.scope);
|
|
216
|
+
}).map((entry) => ({
|
|
217
|
+
id: entry.request.id,
|
|
218
|
+
tool: entry.request.tool,
|
|
219
|
+
risk: entry.request.risk,
|
|
220
|
+
input: entry.request.input,
|
|
221
|
+
digest: entry.request.digest,
|
|
222
|
+
proposedBy: entry.request.actor,
|
|
223
|
+
requestedBy: entry.proposer.id,
|
|
224
|
+
canDecide: whyNotDecide(caller, entry) === void 0
|
|
225
|
+
}));
|
|
226
|
+
return json({ approvals: list });
|
|
227
|
+
}
|
|
228
|
+
if (route === "approvals" && id !== void 0) {
|
|
229
|
+
let requestId;
|
|
230
|
+
try {
|
|
231
|
+
requestId = decodeURIComponent(id);
|
|
232
|
+
} catch {
|
|
233
|
+
return fail(404, "no pending approval with this id");
|
|
234
|
+
}
|
|
235
|
+
return decide(request, caller, requestId, verb === "approve");
|
|
236
|
+
}
|
|
237
|
+
const body = await readJson(request);
|
|
238
|
+
const message = body?.message;
|
|
239
|
+
if (typeof message !== "string" || message.trim() === "" || message.length > MAX_MESSAGE_CHARS) {
|
|
240
|
+
return fail(400, `message must be a text of 1 to ${MAX_MESSAGE_CHARS} characters`);
|
|
241
|
+
}
|
|
242
|
+
if ((running.get(caller.id) ?? 0) >= maxRuns) return fail(429, "too many chat turns at once", { "retry-after": "5" });
|
|
243
|
+
return chat(request, caller, message);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/worker.ts
|
|
249
|
+
var health = (env) => Response.json({ ok: true, pack: env.PACK_NAME ?? "agentic-assistant", version: VERSION });
|
|
250
|
+
function createWorker(options) {
|
|
251
|
+
const api = createAssistantApi(options);
|
|
252
|
+
return {
|
|
253
|
+
async fetch(request, env = {}) {
|
|
254
|
+
const url = new URL(request.url);
|
|
255
|
+
if (request.method === "GET" && url.pathname === "/health") return health(env);
|
|
256
|
+
if (url.pathname.startsWith("/api/v1/")) return api.fetch(request);
|
|
257
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
var worker_default = {
|
|
262
|
+
async fetch(request, env) {
|
|
263
|
+
const url = new URL(request.url);
|
|
264
|
+
if (request.method === "GET" && url.pathname === "/health") return health(env);
|
|
265
|
+
if (url.pathname.startsWith("/api/v1/")) {
|
|
266
|
+
return Response.json({ error: "the API is not set up: build the Worker with createWorker" }, { status: 503 });
|
|
267
|
+
}
|
|
268
|
+
return Response.json({ error: "not found" }, { status: 404 });
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
export {
|
|
272
|
+
APPROVER_SCOPE,
|
|
273
|
+
createAssistantApi,
|
|
274
|
+
createWorker,
|
|
275
|
+
worker_default as default
|
|
276
|
+
};
|
package/llms.txt
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# agentic-assistant
|
|
2
|
+
|
|
3
|
+
An AI assistant that acts inside your apps with the user's own permissions.
|
|
4
|
+
|
|
5
|
+
Keywords: assistant,ai,agent,tools,mcp,sdk,side-panel,approvals,eu,model-port.
|
|
6
|
+
|
|
7
|
+
- SDK: import { runAssistant, ToolRegistry, createPrincipal } from "agentic-assistant".
|
|
8
|
+
- CLI: agentic-assistant chat --tools <mcp-url> --scope <s> --tool-scope <s> "message". MCP server: agentic-assistant mcp.
|
|
9
|
+
- HTTP API: import { createWorker } from "agentic-assistant/worker". Routes under /api/v1: GET /tools, POST /chat (SSE), GET /approvals, POST /approvals/:id/approve|deny. Sign-in with agentic-identity principals.
|
|
10
|
+
- AI marking: events carry `provenance`; use provenanceLabel and provenanceMarker (JSON-LD, IPTC trainedAlgorithmicMedia). Also in HTTP, CLI --json and MCP results.
|
|
11
|
+
- Panel: import { AssistantPanel } from "agentic-assistant/react"; import "agentic-assistant/react.css".
|
|
12
|
+
- Test provider: import { ScriptedProvider } from "agentic-assistant/testing".
|
|
13
|
+
|
|
14
|
+
## Docs
|
|
15
|
+
|
|
16
|
+
- README: https://github.com/pooriaarab/agentic-assistant
|
|
17
|
+
- License: MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,97 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-assistant",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "An AI assistant that acts inside your apps with the user's own permissions.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"assistant",
|
|
7
|
+
"ai",
|
|
8
|
+
"agent",
|
|
9
|
+
"tools",
|
|
10
|
+
"mcp",
|
|
11
|
+
"sdk",
|
|
12
|
+
"side-panel",
|
|
13
|
+
"approvals",
|
|
14
|
+
"eu",
|
|
15
|
+
"model-port"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Pooria Arab",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/pooriaarab/agentic-assistant.git"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"private": false,
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"llms.txt",
|
|
31
|
+
"SKILL.md",
|
|
32
|
+
"CHANGELOG.md"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"main": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./dist/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./worker": {
|
|
45
|
+
"types": "./dist/worker.d.ts",
|
|
46
|
+
"import": "./dist/worker.js"
|
|
47
|
+
},
|
|
48
|
+
"./testing": {
|
|
49
|
+
"types": "./dist/testing.d.ts",
|
|
50
|
+
"import": "./dist/testing.js"
|
|
51
|
+
},
|
|
52
|
+
"./react": {
|
|
53
|
+
"types": "./dist/react.d.ts",
|
|
54
|
+
"import": "./dist/react.js"
|
|
55
|
+
},
|
|
56
|
+
"./react.css": "./dist/react.css"
|
|
57
|
+
},
|
|
58
|
+
"bin": {
|
|
59
|
+
"agentic-assistant": "./dist/cli.js"
|
|
60
|
+
},
|
|
61
|
+
"scripts": {
|
|
62
|
+
"build": "tsup",
|
|
63
|
+
"typecheck": "tsc --noEmit",
|
|
64
|
+
"test": "vitest run",
|
|
65
|
+
"clean": "rm -rf dist",
|
|
66
|
+
"prepack": "npm run build",
|
|
67
|
+
"ci:local": "npm run build && npm run typecheck && npm test"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@anthropic-ai/sdk": "^1.3.0",
|
|
71
|
+
"@modelcontextprotocol/sdk": "^1.32.2",
|
|
72
|
+
"agentic-identity": "^0.3.0",
|
|
73
|
+
"ajv": "^8.20.0"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@testing-library/dom": "^10.4.2",
|
|
77
|
+
"@testing-library/react": "^16.3.3",
|
|
78
|
+
"@testing-library/user-event": "^14.6.8",
|
|
79
|
+
"@types/node": "^22.0.0",
|
|
80
|
+
"@types/react": "^19.3.0",
|
|
81
|
+
"@types/react-dom": "^19.3.0",
|
|
82
|
+
"jsdom": "^30.1.3",
|
|
83
|
+
"react": "^19.3.1",
|
|
84
|
+
"react-dom": "^19.3.1",
|
|
85
|
+
"tsup": "^8.0.0",
|
|
86
|
+
"typescript": "^5.6.0",
|
|
87
|
+
"vitest": "^3.0.0"
|
|
88
|
+
},
|
|
89
|
+
"peerDependencies": {
|
|
90
|
+
"react": ">=18"
|
|
91
|
+
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"react": {
|
|
94
|
+
"optional": true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|