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 +21 -0
- package/README.md +159 -2
- package/SKILL.md +47 -0
- package/dist/chunk-4QE7E5UQ.js +7 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +44 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +999 -0
- package/dist/loop-CDbbDwSs.d.ts +231 -0
- package/dist/react.css +57 -0
- package/dist/react.d.ts +56 -0
- package/dist/react.js +321 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +35 -0
- package/dist/types-C47SyHH5.d.ts +72 -0
- package/dist/worker.d.ts +8 -0
- package/dist/worker.js +17 -0
- package/llms.txt +14 -0
- package/package.json +88 -4
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { J as JsonSchema, U as Usage, c as Message, M as ModelProvider } from './types-C47SyHH5.js';
|
|
2
|
+
|
|
3
|
+
declare const ACTIONS: readonly ["read", "comment", "write", "admin"];
|
|
4
|
+
type Action = (typeof ACTIONS)[number];
|
|
5
|
+
type Scope = string;
|
|
6
|
+
declare function parseScope(scope: Scope): {
|
|
7
|
+
resource: string;
|
|
8
|
+
action: Action;
|
|
9
|
+
};
|
|
10
|
+
/** True when `grant` allows everything `required` asks for. */
|
|
11
|
+
declare function scopeCovers(grant: Scope, required: Scope): boolean;
|
|
12
|
+
/** True when any granted scope covers the required one. A malformed scope never matches. */
|
|
13
|
+
declare function hasScope(granted: readonly Scope[], required: Scope): boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The acting principal. The assistant always acts as an agent of a signed-in
|
|
17
|
+
* person. The host passes the scopes, already narrowed to what the person
|
|
18
|
+
* granted this agent. The assistant never widens them.
|
|
19
|
+
*/
|
|
20
|
+
interface AssistantPrincipal {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly kind: "agent_of_human";
|
|
23
|
+
/** The person the assistant acts for. */
|
|
24
|
+
readonly parentId: string;
|
|
25
|
+
readonly scopes: readonly Scope[];
|
|
26
|
+
/** Epoch milliseconds. Calls stop at this time. */
|
|
27
|
+
readonly expiresAt?: number;
|
|
28
|
+
/** Host check, run before every tool call. A throw or a true result denies the call. */
|
|
29
|
+
readonly isRevoked?: () => boolean | Promise<boolean>;
|
|
30
|
+
}
|
|
31
|
+
interface PrincipalInput {
|
|
32
|
+
id: string;
|
|
33
|
+
kind?: string;
|
|
34
|
+
parentId: string;
|
|
35
|
+
scopes: readonly Scope[];
|
|
36
|
+
expiresAt?: number;
|
|
37
|
+
isRevoked?: () => boolean | Promise<boolean>;
|
|
38
|
+
}
|
|
39
|
+
declare class PrincipalError extends Error {
|
|
40
|
+
constructor(message: string);
|
|
41
|
+
}
|
|
42
|
+
/** Validates host input. A missing or other kind is an error, never a default. */
|
|
43
|
+
declare function createPrincipal(input: PrincipalInput): AssistantPrincipal;
|
|
44
|
+
/** Re-checks a principal at the start of a run. A cast or a plain JS caller cannot skip validation. */
|
|
45
|
+
declare function assertPrincipal(principal: AssistantPrincipal): void;
|
|
46
|
+
/** Who a run, a tool call, and every audit row is attributed to. Always machine-made. */
|
|
47
|
+
interface Actor {
|
|
48
|
+
principalId: string;
|
|
49
|
+
kind: "agent_of_human";
|
|
50
|
+
onBehalfOf: string;
|
|
51
|
+
machineMade: true;
|
|
52
|
+
}
|
|
53
|
+
declare function actorOf(principal: AssistantPrincipal): Actor;
|
|
54
|
+
|
|
55
|
+
/** `safe` runs at once. `risky` and `irreversible` wait for a person. */
|
|
56
|
+
type ToolRisk = "safe" | "risky" | "irreversible";
|
|
57
|
+
declare const TOOL_RISKS: readonly ToolRisk[];
|
|
58
|
+
interface ApprovalRequest {
|
|
59
|
+
id: string;
|
|
60
|
+
tool: string;
|
|
61
|
+
risk: "risky" | "irreversible";
|
|
62
|
+
/** A frozen copy of the arguments the tool will get. */
|
|
63
|
+
input: Readonly<Record<string, unknown>>;
|
|
64
|
+
/** Binds the tool, the arguments, and the acting principal. The decision must echo it. */
|
|
65
|
+
digest: string;
|
|
66
|
+
actor: Actor;
|
|
67
|
+
}
|
|
68
|
+
type ApprovalDecision = {
|
|
69
|
+
requestId: string;
|
|
70
|
+
approved: true;
|
|
71
|
+
digest: string;
|
|
72
|
+
decidedBy: string;
|
|
73
|
+
} | {
|
|
74
|
+
requestId: string;
|
|
75
|
+
approved: false;
|
|
76
|
+
decidedBy?: string;
|
|
77
|
+
reason?: string;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* The approval port. Answer a request with a decision, or fail. The loop treats
|
|
81
|
+
* a failure, a timeout, and a decision that does not match the request as a refusal.
|
|
82
|
+
*/
|
|
83
|
+
interface ApprovalGate {
|
|
84
|
+
request(request: ApprovalRequest, signal: AbortSignal): Promise<ApprovalDecision>;
|
|
85
|
+
}
|
|
86
|
+
/** JSON with sorted keys, so equal values give equal text. */
|
|
87
|
+
declare function canonicalJson(value: unknown): string;
|
|
88
|
+
declare function digestCall(call: {
|
|
89
|
+
tool: string;
|
|
90
|
+
input: unknown;
|
|
91
|
+
principalId: string;
|
|
92
|
+
onBehalfOf: string;
|
|
93
|
+
}): string;
|
|
94
|
+
/**
|
|
95
|
+
* An in-memory gate. `request` waits until `approve` or `reject` is called with the request id.
|
|
96
|
+
* A panel, a CLI, or a test calls them. `approve` signs the stored digest, so a caller cannot
|
|
97
|
+
* approve other arguments than the ones the loop asked about.
|
|
98
|
+
*/
|
|
99
|
+
declare class QueueApprovalGate implements ApprovalGate {
|
|
100
|
+
private readonly waiting;
|
|
101
|
+
pending(): ApprovalRequest[];
|
|
102
|
+
request(request: ApprovalRequest, signal: AbortSignal): Promise<ApprovalDecision>;
|
|
103
|
+
approve(id: string, decidedBy: string): void;
|
|
104
|
+
reject(id: string, decidedBy?: string, reason?: string): void;
|
|
105
|
+
private take;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
interface ToolContext {
|
|
109
|
+
principal: AssistantPrincipal;
|
|
110
|
+
toolUseId: string;
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
}
|
|
113
|
+
interface ToolDefinition<Input = Record<string, unknown>> {
|
|
114
|
+
name: string;
|
|
115
|
+
description: string;
|
|
116
|
+
/** A JSON Schema with `type: "object"`. The loop validates the model's input against it. */
|
|
117
|
+
inputSchema: JsonSchema;
|
|
118
|
+
/** The scope the acting principal must hold to call this tool. */
|
|
119
|
+
scope: Scope;
|
|
120
|
+
/** How much harm a call can do. `risky` and `irreversible` wait for a person's approval. Every tool must say. */
|
|
121
|
+
risk: ToolRisk;
|
|
122
|
+
handler(input: Input, context: ToolContext): Promise<unknown> | unknown;
|
|
123
|
+
}
|
|
124
|
+
/** Types a tool for an SDK function. The loop checks the input against `inputSchema` before it calls `handler`. */
|
|
125
|
+
declare function defineTool<Input>(tool: ToolDefinition<Input>): ToolDefinition;
|
|
126
|
+
declare class ToolRegistry {
|
|
127
|
+
private readonly tools;
|
|
128
|
+
private readonly ajv;
|
|
129
|
+
register(tool: ToolDefinition): this;
|
|
130
|
+
registerAll(tools: readonly ToolDefinition[]): this;
|
|
131
|
+
get(name: string): ToolDefinition | undefined;
|
|
132
|
+
list(): ToolDefinition[];
|
|
133
|
+
/** Returns a short description of the first schema violation, or undefined when the input is valid. */
|
|
134
|
+
checkInput(name: string, input: unknown): string | undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface RunLimits {
|
|
138
|
+
/** Most model turns in one run. Default 8. */
|
|
139
|
+
maxTurns?: number;
|
|
140
|
+
/** Most input plus output tokens, summed over all turns. */
|
|
141
|
+
maxTotalTokens?: number;
|
|
142
|
+
/** Most estimated cost in US dollars. Needs `pricing` on the provider. */
|
|
143
|
+
maxCostUsd?: number;
|
|
144
|
+
/** Output token ceiling for one turn. Default 4096. */
|
|
145
|
+
maxOutputTokensPerTurn?: number;
|
|
146
|
+
/** Time to wait for a person to decide. No decision in time means the call is refused. Default 300000. */
|
|
147
|
+
approvalTimeoutMs?: number;
|
|
148
|
+
/** Most tool calls the model may make in one turn. A larger turn runs none. Default 16. */
|
|
149
|
+
maxToolCallsPerTurn?: number;
|
|
150
|
+
/** Longest tool result, in characters, sent to the model. Longer results are cut. Default 32768. */
|
|
151
|
+
maxToolResultChars?: number;
|
|
152
|
+
}
|
|
153
|
+
interface RunOptions {
|
|
154
|
+
provider: ModelProvider;
|
|
155
|
+
registry: ToolRegistry;
|
|
156
|
+
principal: AssistantPrincipal;
|
|
157
|
+
/** The conversation so far, ending with the new user message. */
|
|
158
|
+
messages: readonly Message[];
|
|
159
|
+
system?: string;
|
|
160
|
+
/** Asked before a `risky` or `irreversible` tool runs. Without it, those tools are not offered and are refused. */
|
|
161
|
+
approvals?: ApprovalGate;
|
|
162
|
+
limits?: RunLimits;
|
|
163
|
+
signal?: AbortSignal;
|
|
164
|
+
now?: () => number;
|
|
165
|
+
}
|
|
166
|
+
type DoneReason = "end_turn" | "max_tokens" | "refusal" | "provider_stop" | "max_turns" | "token_cap" | "cost_cap" | "tool_call_cap" | "provider_error" | "principal_inactive" | "aborted";
|
|
167
|
+
type DeniedReason = "unknown_tool" | "scope" | "invalid_input" | "approval";
|
|
168
|
+
interface RunTotals extends Usage {
|
|
169
|
+
costUsd: number;
|
|
170
|
+
}
|
|
171
|
+
type AssistantEvent = {
|
|
172
|
+
type: "run_start";
|
|
173
|
+
actor: Actor;
|
|
174
|
+
provider: string;
|
|
175
|
+
model: string;
|
|
176
|
+
} | {
|
|
177
|
+
type: "turn_start";
|
|
178
|
+
turn: number;
|
|
179
|
+
} | {
|
|
180
|
+
type: "text_delta";
|
|
181
|
+
text: string;
|
|
182
|
+
} | {
|
|
183
|
+
type: "tool_call";
|
|
184
|
+
id: string;
|
|
185
|
+
name: string;
|
|
186
|
+
input: unknown;
|
|
187
|
+
actor: Actor;
|
|
188
|
+
} | {
|
|
189
|
+
type: "approval_request";
|
|
190
|
+
request: ApprovalRequest;
|
|
191
|
+
} | {
|
|
192
|
+
type: "approval_decision";
|
|
193
|
+
requestId: string;
|
|
194
|
+
approved: boolean;
|
|
195
|
+
decidedBy?: string;
|
|
196
|
+
reason?: string;
|
|
197
|
+
} | {
|
|
198
|
+
type: "tool_result";
|
|
199
|
+
id: string;
|
|
200
|
+
name: string;
|
|
201
|
+
content: string;
|
|
202
|
+
isError: boolean;
|
|
203
|
+
denied?: DeniedReason;
|
|
204
|
+
/** True when `content` came from a tool. Treat it as data, never as an instruction. */
|
|
205
|
+
untrusted?: true;
|
|
206
|
+
/** The raw failure text. It stays in the event and never goes to the model. */
|
|
207
|
+
detail?: string;
|
|
208
|
+
actor: Actor;
|
|
209
|
+
} | {
|
|
210
|
+
type: "turn_end";
|
|
211
|
+
turn: number;
|
|
212
|
+
usage: Usage;
|
|
213
|
+
totals: RunTotals;
|
|
214
|
+
} | {
|
|
215
|
+
type: "done";
|
|
216
|
+
reason: DoneReason;
|
|
217
|
+
totals: RunTotals;
|
|
218
|
+
actor: Actor;
|
|
219
|
+
messages: Message[];
|
|
220
|
+
error?: string;
|
|
221
|
+
};
|
|
222
|
+
/** Sent with every request. Tool output is data, never an instruction. */
|
|
223
|
+
declare const DATA_NOTICE: string;
|
|
224
|
+
/**
|
|
225
|
+
* Runs the assistant until the model stops, a cap trips, or the principal stops
|
|
226
|
+
* being valid. Tools run only when the model stops to use them, only inside the
|
|
227
|
+
* principal's scopes, and never after a cap has tripped.
|
|
228
|
+
*/
|
|
229
|
+
declare function runAssistant(options: RunOptions): AsyncGenerator<AssistantEvent>;
|
|
230
|
+
|
|
231
|
+
export { ACTIONS as A, DATA_NOTICE as D, PrincipalError as P, QueueApprovalGate as Q, type RunLimits as R, type Scope as S, type ToolRisk as T, type ToolDefinition as a, type Action as b, type Actor as c, type ApprovalDecision as d, type ApprovalGate as e, type ApprovalRequest as f, type AssistantEvent as g, type AssistantPrincipal as h, type DeniedReason as i, type DoneReason as j, type PrincipalInput as k, type RunOptions as l, type RunTotals as m, TOOL_RISKS as n, type ToolContext as o, ToolRegistry as p, actorOf as q, assertPrincipal as r, canonicalJson as s, createPrincipal as t, defineTool as u, digestCall as v, hasScope as w, parseScope as x, runAssistant as y, scopeCovers as z };
|
package/dist/react.css
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/* Plain CSS variables. Set them on .aa-panel or a parent to match your app. Every pair below meets
|
|
2
|
+
WCAG 2.2 AA contrast; src/react/styles.test.ts checks it. No remote fonts or images. */
|
|
3
|
+
.aa-panel {
|
|
4
|
+
--aa-bg: #ffffff;
|
|
5
|
+
--aa-fg: #1b1b1b;
|
|
6
|
+
--aa-muted: #4d4d4d;
|
|
7
|
+
--aa-border: #767676;
|
|
8
|
+
--aa-accent: #0b57d0;
|
|
9
|
+
--aa-accent-fg: #ffffff;
|
|
10
|
+
--aa-focus: #0b57d0;
|
|
11
|
+
--aa-machine-bg: #e8eefc;
|
|
12
|
+
--aa-machine-fg: #0a2a6b;
|
|
13
|
+
--aa-card-bg: #f4f4f4;
|
|
14
|
+
--aa-warn-bg: #fff4d6;
|
|
15
|
+
--aa-warn-fg: #5c3d00;
|
|
16
|
+
--aa-danger: #b3261e;
|
|
17
|
+
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
height: 100%;
|
|
21
|
+
min-width: 18rem;
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
background: var(--aa-bg);
|
|
24
|
+
color: var(--aa-fg);
|
|
25
|
+
border: 1px solid var(--aa-border);
|
|
26
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
27
|
+
font-size: 1rem;
|
|
28
|
+
line-height: 1.5;
|
|
29
|
+
}
|
|
30
|
+
.aa-panel *, .aa-panel *::before, .aa-panel *::after { box-sizing: border-box; }
|
|
31
|
+
.aa-header { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--aa-border); }
|
|
32
|
+
.aa-title { margin: 0; font-size: 1.125rem; }
|
|
33
|
+
.aa-log { flex: 1; overflow-y: auto; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.75rem; }
|
|
34
|
+
.aa-empty { color: var(--aa-muted); margin: 0; }
|
|
35
|
+
.aa-msg { padding: 0.5rem 0.75rem; border: 1px solid var(--aa-border); border-radius: 0.5rem; }
|
|
36
|
+
.aa-assistant { background: var(--aa-card-bg); }
|
|
37
|
+
.aa-who { margin: 0 0 0.25rem; font-weight: 600; color: var(--aa-muted); }
|
|
38
|
+
.aa-text { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
39
|
+
.aa-badge { display: inline-block; padding: 0 0.5rem; border-radius: 0.25rem; background: var(--aa-machine-bg); color: var(--aa-machine-fg); font-size: 0.875rem; font-weight: 600; border: 1px solid var(--aa-machine-fg); }
|
|
40
|
+
.aa-card, .aa-approval { padding: 0.5rem 0.75rem; border: 1px solid var(--aa-border); border-radius: 0.5rem; background: var(--aa-card-bg); }
|
|
41
|
+
.aa-card-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
42
|
+
.aa-status { color: var(--aa-muted); font-weight: 600; }
|
|
43
|
+
.aa-pre { margin: 0.25rem 0 0; padding: 0.5rem; max-height: 12rem; overflow: auto; background: var(--aa-bg); border: 1px solid var(--aa-border); white-space: pre-wrap; overflow-wrap: anywhere; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 0.875rem; }
|
|
44
|
+
.aa-approval { background: var(--aa-warn-bg); color: var(--aa-warn-fg); border-width: 2px; border-color: var(--aa-warn-fg); }
|
|
45
|
+
.aa-approval-title { margin: 0 0 0.25rem; font-size: 1rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
46
|
+
.aa-warn { color: var(--aa-danger); background: var(--aa-bg); padding: 0.25rem 0.5rem; border: 2px solid var(--aa-danger); border-radius: 0.25rem; }
|
|
47
|
+
.aa-notice { margin: 0; padding: 0.5rem 0.75rem; border: 1px solid var(--aa-border); border-radius: 0.5rem; }
|
|
48
|
+
.aa-composer { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid var(--aa-border); }
|
|
49
|
+
.aa-label { font-weight: 600; }
|
|
50
|
+
.aa-input { width: 100%; resize: vertical; padding: 0.5rem; font: inherit; color: var(--aa-fg); background: var(--aa-bg); border: 1px solid var(--aa-border); border-radius: 0.25rem; }
|
|
51
|
+
.aa-actions { display: flex; gap: 0.5rem; margin-top: 0.5rem; }
|
|
52
|
+
.aa-composer .aa-actions { margin-top: 0; }
|
|
53
|
+
.aa-button { min-height: 2rem; min-width: 2rem; padding: 0.25rem 1rem; font: inherit; font-weight: 600; color: var(--aa-fg); background: var(--aa-bg); border: 2px solid var(--aa-border); border-radius: 0.25rem; cursor: pointer; }
|
|
54
|
+
.aa-button.aa-primary { color: var(--aa-accent-fg); background: var(--aa-accent); border-color: var(--aa-accent); }
|
|
55
|
+
.aa-button:disabled { cursor: not-allowed; opacity: 0.6; }
|
|
56
|
+
.aa-panel :focus-visible { outline: 3px solid var(--aa-focus); outline-offset: 2px; }
|
|
57
|
+
@media (forced-colors: active) { .aa-badge, .aa-button, .aa-approval, .aa-warn { border: 2px solid CanvasText; } }
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { g as AssistantEvent, T as ToolRisk } from './loop-CDbbDwSs.js';
|
|
3
|
+
import { c as Message } from './types-C47SyHH5.js';
|
|
4
|
+
|
|
5
|
+
type PanelItem = {
|
|
6
|
+
kind: "user";
|
|
7
|
+
id: string;
|
|
8
|
+
text: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "assistant";
|
|
11
|
+
id: string;
|
|
12
|
+
text: string;
|
|
13
|
+
streaming: boolean;
|
|
14
|
+
} | {
|
|
15
|
+
kind: "tool";
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
input: unknown;
|
|
19
|
+
status: "running" | "waiting" | "done" | "failed" | "denied" | "not_run";
|
|
20
|
+
/** Text a tool returned. It is data. Show it as text. */
|
|
21
|
+
content?: string;
|
|
22
|
+
} | {
|
|
23
|
+
kind: "approval";
|
|
24
|
+
id: string;
|
|
25
|
+
tool: string;
|
|
26
|
+
risk: Exclude<ToolRisk, "safe">;
|
|
27
|
+
input: unknown;
|
|
28
|
+
/** `expired`: the run ended before a decision was used. `unavailable`: the approval did not complete. */
|
|
29
|
+
status: "pending" | "approved" | "declined" | "unavailable" | "expired";
|
|
30
|
+
busy: boolean;
|
|
31
|
+
decidedBy?: string;
|
|
32
|
+
} | {
|
|
33
|
+
kind: "notice";
|
|
34
|
+
id: string;
|
|
35
|
+
text: string;
|
|
36
|
+
};
|
|
37
|
+
interface UseAssistantOptions {
|
|
38
|
+
/** Runs the assistant for this conversation. The host owns the model, the tools, and the principal. */
|
|
39
|
+
run(messages: Message[], signal: AbortSignal): AsyncIterable<AssistantEvent>;
|
|
40
|
+
/** Passes a person's decision to the approval gate. The host owns the gate and knows who the person is. */
|
|
41
|
+
decide(requestId: string, approved: boolean, reason?: string): void | Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
declare function useAssistant(options: UseAssistantOptions): {
|
|
44
|
+
items: PanelItem[];
|
|
45
|
+
running: boolean;
|
|
46
|
+
send: (text: string) => Promise<void>;
|
|
47
|
+
stop: () => void | undefined;
|
|
48
|
+
decide: (requestId: string, approved: boolean, reason?: string) => Promise<boolean>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
interface AssistantPanelProps extends UseAssistantOptions {
|
|
52
|
+
title?: string;
|
|
53
|
+
}
|
|
54
|
+
declare function AssistantPanel({ title, run, decide }: AssistantPanelProps): react.JSX.Element;
|
|
55
|
+
|
|
56
|
+
export { AssistantPanel, type AssistantPanelProps, type PanelItem, type UseAssistantOptions, useAssistant };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
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
|
+
const toolItems = /* @__PURE__ */ new Map();
|
|
45
|
+
let lastToolItem = null;
|
|
46
|
+
try {
|
|
47
|
+
for await (const event of latest.current.run(sent, abort.signal)) {
|
|
48
|
+
switch (event.type) {
|
|
49
|
+
case "turn_start":
|
|
50
|
+
streamingId = null;
|
|
51
|
+
break;
|
|
52
|
+
case "text_delta": {
|
|
53
|
+
if (streamingId === null) {
|
|
54
|
+
streamingId = nextId();
|
|
55
|
+
add({ kind: "assistant", id: streamingId, text: event.text, streaming: true });
|
|
56
|
+
} else {
|
|
57
|
+
update(streamingId, (item) => item.kind === "assistant" ? { ...item, text: item.text + event.text } : item);
|
|
58
|
+
}
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
case "turn_end":
|
|
62
|
+
if (streamingId !== null) update(streamingId, (item) => item.kind === "assistant" ? { ...item, streaming: false } : item);
|
|
63
|
+
streamingId = null;
|
|
64
|
+
break;
|
|
65
|
+
case "tool_call": {
|
|
66
|
+
const id = nextId();
|
|
67
|
+
toolItems.set(event.id, id);
|
|
68
|
+
lastToolItem = id;
|
|
69
|
+
add({ kind: "tool", id, name: event.name, input: event.input, status: "running" });
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "approval_request": {
|
|
73
|
+
const { request } = event;
|
|
74
|
+
add({ kind: "approval", id: request.id, tool: request.tool, risk: request.risk, input: request.input, status: "pending", busy: false });
|
|
75
|
+
const waitingId = lastToolItem;
|
|
76
|
+
if (waitingId !== null) update(waitingId, (item) => item.kind === "tool" && item.status === "running" ? { ...item, status: "waiting" } : item);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "approval_decision":
|
|
80
|
+
update(
|
|
81
|
+
event.requestId,
|
|
82
|
+
(item) => item.kind === "approval" ? {
|
|
83
|
+
...item,
|
|
84
|
+
status: event.approved ? "approved" : event.reason === "the person declined" ? "declined" : "unavailable",
|
|
85
|
+
busy: false,
|
|
86
|
+
decidedBy: event.decidedBy
|
|
87
|
+
} : item
|
|
88
|
+
);
|
|
89
|
+
break;
|
|
90
|
+
case "tool_result": {
|
|
91
|
+
const id = toolItems.get(event.id);
|
|
92
|
+
if (id !== void 0) {
|
|
93
|
+
update(
|
|
94
|
+
id,
|
|
95
|
+
(item) => item.kind === "tool" ? { ...item, status: event.denied ? "denied" : event.isError ? "failed" : "done", content: event.content } : item
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case "done": {
|
|
101
|
+
finished = true;
|
|
102
|
+
history.current = event.messages;
|
|
103
|
+
const notice = STOP_NOTICES[event.reason];
|
|
104
|
+
if (notice !== void 0) add({ kind: "notice", id: nextId(), text: notice });
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
default:
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
add({ kind: "notice", id: nextId(), text: RUN_FAILED });
|
|
113
|
+
} finally {
|
|
114
|
+
if (!finished) history.current = sent;
|
|
115
|
+
setItems(
|
|
116
|
+
(current) => current.map((item) => {
|
|
117
|
+
if (item.kind === "approval" && item.status === "pending") return { ...item, status: "expired", busy: false };
|
|
118
|
+
if (item.kind === "tool" && (item.status === "running" || item.status === "waiting")) return { ...item, status: "not_run" };
|
|
119
|
+
return item;
|
|
120
|
+
})
|
|
121
|
+
);
|
|
122
|
+
busy.current = false;
|
|
123
|
+
controller.current = null;
|
|
124
|
+
setRunning(false);
|
|
125
|
+
}
|
|
126
|
+
}, []);
|
|
127
|
+
const stop = useCallback(() => controller.current?.abort(), []);
|
|
128
|
+
useEffect(() => () => controller.current?.abort(), []);
|
|
129
|
+
const decide = useCallback(async (requestId, approved, reason) => {
|
|
130
|
+
update(requestId, (item) => item.kind === "approval" ? { ...item, busy: true } : item);
|
|
131
|
+
try {
|
|
132
|
+
await latest.current.decide(requestId, approved, reason);
|
|
133
|
+
return true;
|
|
134
|
+
} catch {
|
|
135
|
+
update(requestId, (item) => item.kind === "approval" ? { ...item, busy: false } : item);
|
|
136
|
+
add({ kind: "notice", id: nextId(), text: DECIDE_FAILED });
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}, []);
|
|
140
|
+
return { items, running, send, stop, decide };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/react/panel.tsx
|
|
144
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
145
|
+
function MachineMade() {
|
|
146
|
+
return /* @__PURE__ */ jsx("span", { className: "aa-badge", children: "Machine-made" });
|
|
147
|
+
}
|
|
148
|
+
var pretty = (value) => {
|
|
149
|
+
try {
|
|
150
|
+
return JSON.stringify(value, null, 2) ?? "null";
|
|
151
|
+
} catch {
|
|
152
|
+
return "[arguments cannot be shown]";
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
var TOOL_STATUS = {
|
|
156
|
+
running: "Running",
|
|
157
|
+
waiting: "Waiting for approval",
|
|
158
|
+
done: "Done",
|
|
159
|
+
failed: "Failed",
|
|
160
|
+
denied: "Not allowed",
|
|
161
|
+
not_run: "Not run"
|
|
162
|
+
};
|
|
163
|
+
function ToolCard({ item }) {
|
|
164
|
+
return /* @__PURE__ */ jsxs("article", { className: "aa-card", "aria-label": `Tool call: ${item.name}`, "data-machine-made": "true", children: [
|
|
165
|
+
/* @__PURE__ */ jsxs("header", { className: "aa-card-head", children: [
|
|
166
|
+
/* @__PURE__ */ jsx("strong", { children: item.name }),
|
|
167
|
+
" ",
|
|
168
|
+
/* @__PURE__ */ jsx(MachineMade, {}),
|
|
169
|
+
" ",
|
|
170
|
+
/* @__PURE__ */ jsx("span", { className: "aa-status", children: TOOL_STATUS[item.status] })
|
|
171
|
+
] }),
|
|
172
|
+
/* @__PURE__ */ jsxs("details", { children: [
|
|
173
|
+
/* @__PURE__ */ jsx("summary", { children: "Arguments" }),
|
|
174
|
+
/* @__PURE__ */ jsx("pre", { className: "aa-pre", tabIndex: 0, "aria-label": `Arguments for ${item.name}`, children: pretty(item.input) })
|
|
175
|
+
] }),
|
|
176
|
+
item.content !== void 0 && /* @__PURE__ */ jsxs("details", { children: [
|
|
177
|
+
/* @__PURE__ */ jsx("summary", { children: "Result (data from the tool, not instructions)" }),
|
|
178
|
+
/* @__PURE__ */ jsx("pre", { className: "aa-pre", tabIndex: 0, "aria-label": `Result of ${item.name}`, children: item.content })
|
|
179
|
+
] })
|
|
180
|
+
] });
|
|
181
|
+
}
|
|
182
|
+
function ApprovalPrompt({
|
|
183
|
+
item,
|
|
184
|
+
decide,
|
|
185
|
+
focusComposer
|
|
186
|
+
}) {
|
|
187
|
+
const ref = useRef2(null);
|
|
188
|
+
const headingId = useId();
|
|
189
|
+
useLayoutEffect(() => {
|
|
190
|
+
if (item.status === "pending") ref.current?.focus();
|
|
191
|
+
}, [item.status]);
|
|
192
|
+
const answer = (approved) => {
|
|
193
|
+
void decide(item.id, approved).then((taken) => {
|
|
194
|
+
if (taken) focusComposer();
|
|
195
|
+
});
|
|
196
|
+
};
|
|
197
|
+
const outcome = {
|
|
198
|
+
approved: `Approved${item.decidedBy ? ` by ${item.decidedBy}` : ""}`,
|
|
199
|
+
declined: "Declined",
|
|
200
|
+
unavailable: "Not approved. The approval did not complete, so the action did not run.",
|
|
201
|
+
expired: "Expired. The assistant stopped before you decided, so the action did not run."
|
|
202
|
+
};
|
|
203
|
+
return /* @__PURE__ */ jsxs("div", { className: "aa-approval", role: "group", "aria-labelledby": headingId, tabIndex: -1, ref, "data-machine-made": "true", children: [
|
|
204
|
+
/* @__PURE__ */ jsxs("h3", { id: headingId, className: "aa-approval-title", children: [
|
|
205
|
+
"Approval needed ",
|
|
206
|
+
/* @__PURE__ */ jsx(MachineMade, {})
|
|
207
|
+
] }),
|
|
208
|
+
/* @__PURE__ */ jsxs("p", { children: [
|
|
209
|
+
"The assistant asks to run ",
|
|
210
|
+
/* @__PURE__ */ jsx("strong", { children: item.tool }),
|
|
211
|
+
"."
|
|
212
|
+
] }),
|
|
213
|
+
item.risk === "irreversible" ? /* @__PURE__ */ jsxs("p", { className: "aa-warn", children: [
|
|
214
|
+
/* @__PURE__ */ jsx("strong", { children: "Warning:" }),
|
|
215
|
+
" This cannot be undone."
|
|
216
|
+
] }) : /* @__PURE__ */ jsx("p", { children: "This can change data." }),
|
|
217
|
+
/* @__PURE__ */ jsx("pre", { className: "aa-pre", tabIndex: 0, "aria-label": `Exact arguments for ${item.tool}`, children: pretty(item.input) }),
|
|
218
|
+
item.status === "pending" ? /* @__PURE__ */ jsxs("div", { className: "aa-actions", children: [
|
|
219
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "aa-button aa-primary", disabled: item.busy, "aria-label": `Approve ${item.tool}`, onClick: () => answer(true), children: "Approve" }),
|
|
220
|
+
/* @__PURE__ */ jsx("button", { type: "button", className: "aa-button", disabled: item.busy, "aria-label": `Decline ${item.tool}`, onClick: () => answer(false), children: "Decline" })
|
|
221
|
+
] }) : /* @__PURE__ */ jsx("p", { className: "aa-status", children: outcome[item.status] })
|
|
222
|
+
] });
|
|
223
|
+
}
|
|
224
|
+
function AssistantPanel({ title = "AI assistant", run, decide }) {
|
|
225
|
+
const { items, running, send, stop, decide: decideRequest } = useAssistant({ run, decide });
|
|
226
|
+
const [draft, setDraft] = useState2("");
|
|
227
|
+
const inputId = useId();
|
|
228
|
+
const input = useRef2(null);
|
|
229
|
+
const log = useRef2(null);
|
|
230
|
+
const pinned = useRef2(true);
|
|
231
|
+
useEffect2(() => {
|
|
232
|
+
const el = log.current;
|
|
233
|
+
if (el && pinned.current) el.scrollTop = el.scrollHeight;
|
|
234
|
+
}, [items]);
|
|
235
|
+
const submit = () => {
|
|
236
|
+
if (draft.trim() === "" || running) return;
|
|
237
|
+
const text = draft;
|
|
238
|
+
setDraft("");
|
|
239
|
+
void send(text);
|
|
240
|
+
};
|
|
241
|
+
const onKeyDown = (event) => {
|
|
242
|
+
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
|
243
|
+
event.preventDefault();
|
|
244
|
+
submit();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
return /* @__PURE__ */ jsxs("section", { className: "aa-panel", "aria-label": title, children: [
|
|
248
|
+
/* @__PURE__ */ jsxs("header", { className: "aa-header", children: [
|
|
249
|
+
/* @__PURE__ */ jsx("h2", { className: "aa-title", children: title }),
|
|
250
|
+
/* @__PURE__ */ jsx(MachineMade, {})
|
|
251
|
+
] }),
|
|
252
|
+
/* @__PURE__ */ jsxs(
|
|
253
|
+
"div",
|
|
254
|
+
{
|
|
255
|
+
className: "aa-log",
|
|
256
|
+
role: "log",
|
|
257
|
+
"aria-live": "polite",
|
|
258
|
+
"aria-relevant": "additions text",
|
|
259
|
+
"aria-label": "Conversation",
|
|
260
|
+
tabIndex: 0,
|
|
261
|
+
ref: log,
|
|
262
|
+
onScroll: (event) => {
|
|
263
|
+
const el = event.currentTarget;
|
|
264
|
+
pinned.current = el.scrollHeight - el.scrollTop - el.clientHeight < 24;
|
|
265
|
+
},
|
|
266
|
+
children: [
|
|
267
|
+
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." }),
|
|
268
|
+
items.map((item) => {
|
|
269
|
+
switch (item.kind) {
|
|
270
|
+
case "user":
|
|
271
|
+
return /* @__PURE__ */ jsxs("div", { className: "aa-msg aa-user", children: [
|
|
272
|
+
/* @__PURE__ */ jsx("p", { className: "aa-who", children: "You" }),
|
|
273
|
+
/* @__PURE__ */ jsx("p", { className: "aa-text", children: item.text })
|
|
274
|
+
] }, item.id);
|
|
275
|
+
case "assistant":
|
|
276
|
+
return /* @__PURE__ */ jsxs("div", { className: "aa-msg aa-assistant", "data-machine-made": "true", "aria-busy": item.streaming, children: [
|
|
277
|
+
/* @__PURE__ */ jsxs("p", { className: "aa-who", children: [
|
|
278
|
+
"Assistant ",
|
|
279
|
+
/* @__PURE__ */ jsx(MachineMade, {})
|
|
280
|
+
] }),
|
|
281
|
+
/* @__PURE__ */ jsx("p", { className: "aa-text", children: item.text })
|
|
282
|
+
] }, item.id);
|
|
283
|
+
case "tool":
|
|
284
|
+
return /* @__PURE__ */ jsx(ToolCard, { item }, item.id);
|
|
285
|
+
case "approval":
|
|
286
|
+
return /* @__PURE__ */ jsx(ApprovalPrompt, { item, decide: decideRequest, focusComposer: () => input.current?.focus() }, item.id);
|
|
287
|
+
case "notice":
|
|
288
|
+
return /* @__PURE__ */ jsxs("p", { className: "aa-notice", children: [
|
|
289
|
+
/* @__PURE__ */ jsx("strong", { children: "Notice:" }),
|
|
290
|
+
" ",
|
|
291
|
+
item.text
|
|
292
|
+
] }, item.id);
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
]
|
|
296
|
+
}
|
|
297
|
+
),
|
|
298
|
+
/* @__PURE__ */ jsxs(
|
|
299
|
+
"form",
|
|
300
|
+
{
|
|
301
|
+
className: "aa-composer",
|
|
302
|
+
onSubmit: (event) => {
|
|
303
|
+
event.preventDefault();
|
|
304
|
+
submit();
|
|
305
|
+
},
|
|
306
|
+
children: [
|
|
307
|
+
/* @__PURE__ */ jsx("label", { htmlFor: inputId, className: "aa-label", children: "Message the assistant" }),
|
|
308
|
+
/* @__PURE__ */ jsx("textarea", { id: inputId, className: "aa-input", rows: 3, value: draft, ref: input, onChange: (event) => setDraft(event.target.value), onKeyDown }),
|
|
309
|
+
/* @__PURE__ */ jsxs("div", { className: "aa-actions", children: [
|
|
310
|
+
/* @__PURE__ */ jsx("button", { type: "submit", className: "aa-button aa-primary", disabled: running || draft.trim() === "", children: "Send" }),
|
|
311
|
+
running && /* @__PURE__ */ jsx("button", { type: "button", className: "aa-button", onClick: stop, children: "Stop" })
|
|
312
|
+
] })
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
)
|
|
316
|
+
] });
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
AssistantPanel,
|
|
320
|
+
useAssistant
|
|
321
|
+
};
|