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/dist/react.js ADDED
@@ -0,0 +1,327 @@
1
+ // src/react/panel.tsx
2
+ import { useEffect as useEffect2, useId, useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
3
+
4
+ // src/react/use-assistant.ts
5
+ import { useCallback, useEffect, useRef, useState } from "react";
6
+ var STOP_NOTICES = {
7
+ max_turns: "The assistant stopped because it reached its step limit. Your messages are kept. Send a message to go on.",
8
+ token_cap: "The assistant stopped because the conversation reached its size limit. Your messages are kept.",
9
+ cost_cap: "The assistant stopped because it reached its cost limit. Your messages are kept.",
10
+ max_tokens: "The answer was cut short because it was too long. Ask for a shorter answer.",
11
+ refusal: "The model did not answer this request. Try to ask in another way.",
12
+ provider_error: "The assistant could not get an answer from the model. Your messages are kept. Try again.",
13
+ principal_inactive: "The assistant no longer has permission to act for you. Sign in again, then try again.",
14
+ tool_call_cap: "The assistant tried to use too many tools at once, so it used none. Your messages are kept.",
15
+ provider_stop: "The model stopped for a reason that is not known. Your messages are kept. Try again.",
16
+ aborted: "You stopped the assistant. Your messages are kept."
17
+ };
18
+ var RUN_FAILED = "The assistant could not finish. Your messages are kept. Try again, or contact support.";
19
+ var DECIDE_FAILED = "Your decision could not be sent. Nothing ran. Try again.";
20
+ function useAssistant(options) {
21
+ const [items, setItems] = useState([]);
22
+ const [running, setRunning] = useState(false);
23
+ const history = useRef([]);
24
+ const controller = useRef(null);
25
+ const busy = useRef(false);
26
+ const counter = useRef(0);
27
+ const latest = useRef(options);
28
+ latest.current = options;
29
+ const nextId = () => `item-${++counter.current}`;
30
+ const add = (item) => setItems((current) => [...current, item]);
31
+ const update = (id, change) => setItems((current) => current.map((item) => item.id === id ? change(item) : item));
32
+ const send = useCallback(async (text) => {
33
+ const trimmed = text.trim();
34
+ if (trimmed === "" || busy.current) return;
35
+ busy.current = true;
36
+ setRunning(true);
37
+ const userMessage = { role: "user", content: [{ type: "text", text: trimmed }] };
38
+ add({ kind: "user", id: nextId(), text: trimmed });
39
+ const abort = new AbortController();
40
+ controller.current = abort;
41
+ const sent = [...history.current, userMessage];
42
+ let streamingId = null;
43
+ let finished = false;
44
+ let failed = false;
45
+ const toolItems = /* @__PURE__ */ new Map();
46
+ let lastToolItem = null;
47
+ try {
48
+ for await (const event of latest.current.run(sent, abort.signal)) {
49
+ switch (event.type) {
50
+ case "turn_start":
51
+ streamingId = null;
52
+ break;
53
+ case "text_delta": {
54
+ if (streamingId === null) {
55
+ streamingId = nextId();
56
+ add({ kind: "assistant", id: streamingId, text: event.text, streaming: true });
57
+ } else {
58
+ update(streamingId, (item) => item.kind === "assistant" ? { ...item, text: item.text + event.text } : item);
59
+ }
60
+ break;
61
+ }
62
+ case "turn_end":
63
+ if (streamingId !== null) update(streamingId, (item) => item.kind === "assistant" ? { ...item, streaming: false } : item);
64
+ streamingId = null;
65
+ break;
66
+ case "tool_call": {
67
+ const id = nextId();
68
+ toolItems.set(event.id, id);
69
+ lastToolItem = id;
70
+ add({ kind: "tool", id, name: event.name, input: event.input, status: "running" });
71
+ break;
72
+ }
73
+ case "approval_request": {
74
+ const { request } = event;
75
+ add({ kind: "approval", id: request.id, tool: request.tool, risk: request.risk, input: request.input, status: "pending", busy: false });
76
+ const waitingId = lastToolItem;
77
+ if (waitingId !== null) update(waitingId, (item) => item.kind === "tool" && item.status === "running" ? { ...item, status: "waiting" } : item);
78
+ break;
79
+ }
80
+ case "approval_decision":
81
+ update(
82
+ event.requestId,
83
+ (item) => item.kind === "approval" ? {
84
+ ...item,
85
+ status: event.approved ? "approved" : event.declined ? "declined" : "unavailable",
86
+ busy: false,
87
+ decidedBy: event.decidedBy
88
+ } : item
89
+ );
90
+ break;
91
+ case "tool_result": {
92
+ const id = toolItems.get(event.id);
93
+ if (id !== void 0) {
94
+ update(
95
+ id,
96
+ (item) => item.kind === "tool" ? { ...item, status: event.denied ? "denied" : event.isError ? "failed" : "done", content: event.content } : item
97
+ );
98
+ }
99
+ break;
100
+ }
101
+ case "done": {
102
+ finished = true;
103
+ history.current = event.messages;
104
+ const notice = STOP_NOTICES[event.reason];
105
+ if (notice !== void 0) add({ kind: "notice", id: nextId(), text: notice });
106
+ break;
107
+ }
108
+ default:
109
+ break;
110
+ }
111
+ }
112
+ } catch {
113
+ failed = true;
114
+ } finally {
115
+ if (!finished) {
116
+ history.current = sent;
117
+ if (!failed) failed = true;
118
+ }
119
+ if (failed) add({ kind: "notice", id: nextId(), text: RUN_FAILED });
120
+ setItems(
121
+ (current) => current.map((item) => {
122
+ if (item.kind === "assistant" && item.streaming) return { ...item, streaming: false };
123
+ if (item.kind === "approval" && item.status === "pending") return { ...item, status: "expired", busy: false };
124
+ if (item.kind === "tool" && (item.status === "running" || item.status === "waiting")) return { ...item, status: "not_run" };
125
+ return item;
126
+ })
127
+ );
128
+ busy.current = false;
129
+ controller.current = null;
130
+ setRunning(false);
131
+ }
132
+ }, []);
133
+ const stop = useCallback(() => controller.current?.abort(), []);
134
+ useEffect(() => () => controller.current?.abort(), []);
135
+ const decide = useCallback(async (requestId, approved, reason) => {
136
+ update(requestId, (item) => item.kind === "approval" ? { ...item, busy: true } : item);
137
+ try {
138
+ await latest.current.decide(requestId, approved, reason);
139
+ return true;
140
+ } catch {
141
+ update(requestId, (item) => item.kind === "approval" ? { ...item, busy: false } : item);
142
+ add({ kind: "notice", id: nextId(), text: DECIDE_FAILED });
143
+ return false;
144
+ }
145
+ }, []);
146
+ return { items, running, send, stop, decide };
147
+ }
148
+
149
+ // src/react/panel.tsx
150
+ import { jsx, jsxs } from "react/jsx-runtime";
151
+ function MachineMade() {
152
+ return /* @__PURE__ */ jsx("span", { className: "aa-badge", children: "Machine-made" });
153
+ }
154
+ var pretty = (value) => {
155
+ try {
156
+ return JSON.stringify(value, null, 2) ?? "null";
157
+ } catch {
158
+ return "[arguments cannot be shown]";
159
+ }
160
+ };
161
+ var TOOL_STATUS = {
162
+ running: "Running",
163
+ waiting: "Waiting for approval",
164
+ done: "Done",
165
+ failed: "Failed",
166
+ denied: "Not allowed",
167
+ not_run: "Not run"
168
+ };
169
+ function ToolCard({ item }) {
170
+ return /* @__PURE__ */ jsxs("article", { className: "aa-card", "aria-label": `Tool call: ${item.name}`, "data-machine-made": "true", children: [
171
+ /* @__PURE__ */ jsxs("header", { className: "aa-card-head", children: [
172
+ /* @__PURE__ */ jsx("strong", { children: item.name }),
173
+ " ",
174
+ /* @__PURE__ */ jsx(MachineMade, {}),
175
+ " ",
176
+ /* @__PURE__ */ jsx("span", { className: "aa-status", children: TOOL_STATUS[item.status] })
177
+ ] }),
178
+ /* @__PURE__ */ jsxs("details", { children: [
179
+ /* @__PURE__ */ jsx("summary", { children: "Arguments" }),
180
+ /* @__PURE__ */ jsx("pre", { className: "aa-pre", tabIndex: 0, "aria-label": `Arguments for ${item.name}`, children: pretty(item.input) })
181
+ ] }),
182
+ item.content !== void 0 && /* @__PURE__ */ jsxs("details", { children: [
183
+ /* @__PURE__ */ jsx("summary", { children: "Result (data from the tool, not instructions)" }),
184
+ /* @__PURE__ */ jsx("pre", { className: "aa-pre", tabIndex: 0, "aria-label": `Result of ${item.name}`, children: item.content })
185
+ ] })
186
+ ] });
187
+ }
188
+ function ApprovalPrompt({
189
+ item,
190
+ decide,
191
+ focusComposer
192
+ }) {
193
+ const ref = useRef2(null);
194
+ const headingId = useId();
195
+ useLayoutEffect(() => {
196
+ if (item.status === "pending") ref.current?.focus();
197
+ }, [item.status]);
198
+ const answer = (approved) => {
199
+ void decide(item.id, approved).then((taken) => {
200
+ if (taken) focusComposer();
201
+ });
202
+ };
203
+ const outcome = {
204
+ approved: `Approved${item.decidedBy ? ` by ${item.decidedBy}` : ""}`,
205
+ declined: "Declined",
206
+ unavailable: "Not approved. The approval did not complete, so the action did not run.",
207
+ expired: "Expired. The assistant stopped before you decided, so the action did not run."
208
+ };
209
+ return /* @__PURE__ */ jsxs("div", { className: "aa-approval", role: "group", "aria-labelledby": headingId, tabIndex: -1, ref, "data-machine-made": "true", children: [
210
+ /* @__PURE__ */ jsxs("h3", { id: headingId, className: "aa-approval-title", children: [
211
+ "Approval needed ",
212
+ /* @__PURE__ */ jsx(MachineMade, {})
213
+ ] }),
214
+ /* @__PURE__ */ jsxs("p", { children: [
215
+ "The assistant asks to run ",
216
+ /* @__PURE__ */ jsx("strong", { children: item.tool }),
217
+ "."
218
+ ] }),
219
+ item.risk === "irreversible" ? /* @__PURE__ */ jsxs("p", { className: "aa-warn", children: [
220
+ /* @__PURE__ */ jsx("strong", { children: "Warning:" }),
221
+ " This cannot be undone."
222
+ ] }) : /* @__PURE__ */ jsx("p", { children: "This can change data." }),
223
+ /* @__PURE__ */ jsx("pre", { className: "aa-pre", tabIndex: 0, "aria-label": `Exact arguments for ${item.tool}`, children: pretty(item.input) }),
224
+ item.status === "pending" ? /* @__PURE__ */ jsxs("div", { className: "aa-actions", children: [
225
+ /* @__PURE__ */ jsx("button", { type: "button", className: "aa-button aa-primary", disabled: item.busy, "aria-label": `Approve ${item.tool}`, onClick: () => answer(true), children: "Approve" }),
226
+ /* @__PURE__ */ jsx("button", { type: "button", className: "aa-button", disabled: item.busy, "aria-label": `Decline ${item.tool}`, onClick: () => answer(false), children: "Decline" })
227
+ ] }) : /* @__PURE__ */ jsx("p", { className: "aa-status", children: outcome[item.status] })
228
+ ] });
229
+ }
230
+ function AssistantPanel({ title = "AI assistant", run, decide }) {
231
+ const { items, running, send, stop, decide: decideRequest } = useAssistant({ run, decide });
232
+ const [draft, setDraft] = useState2("");
233
+ const inputId = useId();
234
+ const input = useRef2(null);
235
+ const log = useRef2(null);
236
+ const pinned = useRef2(true);
237
+ useEffect2(() => {
238
+ const el = log.current;
239
+ if (el && pinned.current) el.scrollTop = el.scrollHeight;
240
+ }, [items]);
241
+ const submit = () => {
242
+ if (draft.trim() === "" || running) return;
243
+ const text = draft;
244
+ setDraft("");
245
+ void send(text);
246
+ };
247
+ const onKeyDown = (event) => {
248
+ if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
249
+ event.preventDefault();
250
+ submit();
251
+ }
252
+ };
253
+ return /* @__PURE__ */ jsxs("section", { className: "aa-panel", "aria-label": title, children: [
254
+ /* @__PURE__ */ jsxs("header", { className: "aa-header", children: [
255
+ /* @__PURE__ */ jsx("h2", { className: "aa-title", children: title }),
256
+ /* @__PURE__ */ jsx(MachineMade, {})
257
+ ] }),
258
+ /* @__PURE__ */ jsxs(
259
+ "div",
260
+ {
261
+ className: "aa-log",
262
+ role: "log",
263
+ "aria-live": "polite",
264
+ "aria-relevant": "additions text",
265
+ "aria-label": "Conversation",
266
+ tabIndex: 0,
267
+ ref: log,
268
+ onScroll: (event) => {
269
+ const el = event.currentTarget;
270
+ pinned.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
271
+ },
272
+ children: [
273
+ items.length === 0 && /* @__PURE__ */ jsx("p", { className: "aa-empty", children: "Ask the assistant to help. It acts only with your permissions, and it asks you before a risky action." }),
274
+ items.map((item) => {
275
+ switch (item.kind) {
276
+ case "user":
277
+ return /* @__PURE__ */ jsxs("div", { className: "aa-msg aa-user", children: [
278
+ /* @__PURE__ */ jsx("p", { className: "aa-who", children: "You" }),
279
+ /* @__PURE__ */ jsx("p", { className: "aa-text", children: item.text })
280
+ ] }, item.id);
281
+ case "assistant":
282
+ return /* @__PURE__ */ jsxs("div", { className: "aa-msg aa-assistant", "data-machine-made": "true", "aria-busy": item.streaming, children: [
283
+ /* @__PURE__ */ jsxs("p", { className: "aa-who", children: [
284
+ "Assistant ",
285
+ /* @__PURE__ */ jsx(MachineMade, {})
286
+ ] }),
287
+ /* @__PURE__ */ jsx("p", { className: "aa-text", children: item.text })
288
+ ] }, item.id);
289
+ case "tool":
290
+ return /* @__PURE__ */ jsx(ToolCard, { item }, item.id);
291
+ case "approval":
292
+ return /* @__PURE__ */ jsx(ApprovalPrompt, { item, decide: decideRequest, focusComposer: () => input.current?.focus() }, item.id);
293
+ case "notice":
294
+ return /* @__PURE__ */ jsxs("p", { className: "aa-notice", children: [
295
+ /* @__PURE__ */ jsx("strong", { children: "Notice:" }),
296
+ " ",
297
+ item.text
298
+ ] }, item.id);
299
+ }
300
+ })
301
+ ]
302
+ }
303
+ ),
304
+ /* @__PURE__ */ jsxs(
305
+ "form",
306
+ {
307
+ className: "aa-composer",
308
+ onSubmit: (event) => {
309
+ event.preventDefault();
310
+ submit();
311
+ },
312
+ children: [
313
+ /* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "aa-label", children: "Message the assistant" }),
314
+ /* @__PURE__ */ jsx("textarea", { id: inputId, className: "aa-input", rows: 3, value: draft, ref: input, onChange: (event) => setDraft(event.target.value), onKeyDown }),
315
+ /* @__PURE__ */ jsxs("div", { className: "aa-actions", children: [
316
+ /* @__PURE__ */ jsx("button", { type: "submit", className: "aa-button aa-primary", disabled: running || draft.trim() === "", children: "Send" }),
317
+ running && /* @__PURE__ */ jsx("button", { type: "button", className: "aa-button", onClick: stop, children: "Stop" })
318
+ ] })
319
+ ]
320
+ }
321
+ )
322
+ ] });
323
+ }
324
+ export {
325
+ AssistantPanel,
326
+ useAssistant
327
+ };
@@ -0,0 +1,29 @@
1
+ import { M as ModelProvider, P as Pricing, a as ModelRequest, S as StopReason, U as Usage, b as ModelEvent } from './types-DGH8FS56.js';
2
+
3
+ interface ScriptedTurn {
4
+ text?: string;
5
+ toolCalls?: {
6
+ name: string;
7
+ input: unknown;
8
+ id?: string;
9
+ }[];
10
+ /** Default: "tool_use" when there are tool calls, else "end_turn". */
11
+ stopReason?: StopReason;
12
+ usage?: Usage;
13
+ }
14
+ /**
15
+ * A model that replays a fixed script, one turn per request. Use it in E2E
16
+ * tests so runs are deterministic. It records each request it receives.
17
+ */
18
+ declare class ScriptedProvider implements ModelProvider {
19
+ private readonly script;
20
+ readonly pricing?: Pricing | undefined;
21
+ readonly name = "scripted";
22
+ readonly model: string;
23
+ readonly requests: ModelRequest[];
24
+ private next;
25
+ constructor(script: readonly ScriptedTurn[], pricing?: Pricing | undefined);
26
+ stream(request: ModelRequest): AsyncGenerator<ModelEvent>;
27
+ }
28
+
29
+ export { ScriptedProvider, type ScriptedTurn };
@@ -0,0 +1,6 @@
1
+ import {
2
+ ScriptedProvider
3
+ } from "./chunk-HYT732TN.js";
4
+ export {
5
+ ScriptedProvider
6
+ };
@@ -0,0 +1,84 @@
1
+ /** A JSON Schema object that describes one tool's input. */
2
+ type JsonSchema = Record<string, unknown>;
3
+ type ContentBlock = {
4
+ type: "text";
5
+ text: string;
6
+ } | {
7
+ type: "tool_use";
8
+ id: string;
9
+ name: string;
10
+ input: unknown;
11
+ } | {
12
+ type: "tool_result";
13
+ toolUseId: string;
14
+ content: string;
15
+ isError?: boolean;
16
+ }
17
+ /** A block only the named provider reads (for example reasoning). Adapters send it back unchanged. */
18
+ | {
19
+ type: "provider_block";
20
+ provider: string;
21
+ raw: unknown;
22
+ };
23
+ interface Message {
24
+ role: "user" | "assistant";
25
+ content: ContentBlock[];
26
+ }
27
+ interface ToolSpec {
28
+ name: string;
29
+ description: string;
30
+ inputSchema: JsonSchema;
31
+ }
32
+ interface Usage {
33
+ inputTokens: number;
34
+ outputTokens: number;
35
+ }
36
+ type StopReason = "end_turn" | "tool_use" | "max_tokens" | "refusal" | "other";
37
+ interface ModelRequest {
38
+ system?: string;
39
+ messages: Message[];
40
+ tools: ToolSpec[];
41
+ maxOutputTokens: number;
42
+ signal?: AbortSignal;
43
+ }
44
+ /** Which model gave an answer. A provider that picks between models sets it on the final message. */
45
+ interface AnsweredBy {
46
+ provider: string;
47
+ model: string;
48
+ /** True when a fallback model answered. */
49
+ fallback: boolean;
50
+ /** Why the first model did not answer. Set only when `fallback` is true. */
51
+ fallbackFor?: "refusal" | "error";
52
+ }
53
+ type ModelEvent = {
54
+ type: "text_delta";
55
+ text: string;
56
+ } | {
57
+ type: "message";
58
+ content: ContentBlock[];
59
+ stopReason: StopReason;
60
+ usage: Usage;
61
+ answeredBy?: AnsweredBy;
62
+ };
63
+ /** Prices in US dollars per million tokens. */
64
+ interface Pricing {
65
+ inputPerMTok: number;
66
+ outputPerMTok: number;
67
+ }
68
+ /**
69
+ * The model port. An adapter streams text deltas, then yields one final
70
+ * `message` event with the whole assistant turn, the stop reason, and usage.
71
+ */
72
+ interface ModelProvider {
73
+ readonly name: string;
74
+ readonly model: string;
75
+ readonly pricing?: Pricing;
76
+ /** Most requests one `stream` call may send to a model. Default 1. The loop counts the input of each in its caps. */
77
+ readonly maxAsks?: number;
78
+ stream(request: ModelRequest): AsyncIterable<ModelEvent>;
79
+ }
80
+ declare class ProviderError extends Error {
81
+ constructor(message: string);
82
+ }
83
+
84
+ export { type AnsweredBy as A, type ContentBlock as C, type JsonSchema as J, type ModelProvider as M, type Pricing as P, type StopReason as S, type ToolSpec as T, type Usage as U, type ModelRequest as a, type ModelEvent as b, type Message as c, ProviderError as d };
@@ -0,0 +1,41 @@
1
+ import { Auth } from 'agentic-identity';
2
+ import { t as ToolRegistry, R as RunLimits } from './loop-42giKLVa.js';
3
+ import { M as ModelProvider } from './types-DGH8FS56.js';
4
+
5
+ interface AssistantApiOptions {
6
+ /** Reads the credential on a request. Use an `agentic-identity` Auth, such as `createStoreAuth`. */
7
+ auth: Auth;
8
+ /** Makes the model for one chat turn. Called once per request, so a provider may keep state. */
9
+ provider(): ModelProvider;
10
+ /** The tools the assistant may use. Each one has a scope and a risk. */
11
+ registry: ToolRegistry;
12
+ system?: string;
13
+ limits?: RunLimits;
14
+ /** Most chat turns one principal may run at once. Default 4. */
15
+ maxConcurrentRuns?: number;
16
+ }
17
+ /** An agent needs this scope before it may decide on a tool call. A person needs no extra scope. */
18
+ declare const APPROVER_SCOPE = "assistant:admin";
19
+ /**
20
+ * The hosted API, under `/api/v1`: `GET /tools`, `POST /chat` (a stream of server-sent events),
21
+ * `GET /approvals`, and `POST /approvals/:id/approve` or `/deny`. Every route needs a principal.
22
+ *
23
+ * A pending approval lives in the memory of this object. Run chat and approval on one instance.
24
+ */
25
+ declare function createAssistantApi(options: AssistantApiOptions): {
26
+ fetch(request: Request): Promise<Response>;
27
+ };
28
+
29
+ interface WorkerEnv {
30
+ PACK_NAME?: string;
31
+ }
32
+ /** A Worker that serves `/health` and the `/api/v1` routes. Build the options in your own entry file. */
33
+ declare function createWorker(options: AssistantApiOptions): {
34
+ fetch(request: Request, env?: WorkerEnv): Promise<Response>;
35
+ };
36
+ /** The sample Worker has no auth, tools, or model, so it serves `/health` only. Use `createWorker` for the API. */
37
+ declare const _default: {
38
+ fetch(request: Request, env: WorkerEnv): Promise<Response>;
39
+ };
40
+
41
+ export { APPROVER_SCOPE, type AssistantApiOptions, type WorkerEnv, createAssistantApi, createWorker, _default as default };