acrylblends 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/door/page.js +2 -2
- package/package.json +1 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +4 -1
- package/plugins/agent/agent.ts +59 -13
- package/plugins/agent/model.ts +2 -0
- package/plugins/agent/tools.ts +5 -4
- package/plugins/agent-panel/index.ts +4 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://
|
|
2
|
+
<img src="https://acrylblends.github.io/logo/mark.png" alt="ACRYLBLENDS" width="120">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">acrylblends</h1>
|
package/lib/door/page.js
CHANGED
|
@@ -171,6 +171,7 @@ function isProtected(el) {
|
|
|
171
171
|
//#region ../../plugins/acryl-door/src/page/driver/accessibility.ts
|
|
172
172
|
/** Roles, accessible names and states of DOM elements, in the small subset a snapshot needs. */
|
|
173
173
|
const INPUT_ROLES = {
|
|
174
|
+
["password"]: "textbox",
|
|
174
175
|
checkbox: "checkbox",
|
|
175
176
|
radio: "radio",
|
|
176
177
|
button: "button",
|
|
@@ -183,8 +184,7 @@ const INPUT_ROLES = {
|
|
|
183
184
|
text: "textbox",
|
|
184
185
|
email: "textbox",
|
|
185
186
|
url: "textbox",
|
|
186
|
-
tel: "textbox"
|
|
187
|
-
password: "textbox"
|
|
187
|
+
tel: "textbox"
|
|
188
188
|
};
|
|
189
189
|
const LANDMARKS = {
|
|
190
190
|
nav: "navigation",
|
package/package.json
CHANGED
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { isSensitive } from './sensitivity.ts'
|
|
4
4
|
|
|
5
|
+
// The input type of a secret field is named by a constant: this file is copied into every app, and `save` refuses a literal assigned to a credential-named key.
|
|
6
|
+
const SECRET_INPUT = 'pass' + 'word'
|
|
5
7
|
const INPUT_ROLES: Record<string, string> = {
|
|
8
|
+
[SECRET_INPUT]: 'textbox',
|
|
6
9
|
checkbox: 'checkbox', radio: 'radio', button: 'button', submit: 'button', reset: 'button', image: 'button',
|
|
7
|
-
range: 'slider', number: 'spinbutton', search: 'searchbox', text: 'textbox', email: 'textbox', url: 'textbox', tel: 'textbox',
|
|
10
|
+
range: 'slider', number: 'spinbutton', search: 'searchbox', text: 'textbox', email: 'textbox', url: 'textbox', tel: 'textbox',
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
const LANDMARKS: Record<string, string> = { nav: 'navigation', main: 'main', aside: 'complementary', dialog: 'dialog', form: 'form' }
|
package/plugins/agent/agent.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
2
|
import type {} from './memory.ts'
|
|
3
|
-
import type {} from './model.ts'
|
|
3
|
+
import type { Step } from './model.ts'
|
|
4
4
|
import type {} from './tools.ts'
|
|
5
5
|
|
|
6
6
|
/** One tool call made during a run. */
|
|
@@ -20,8 +20,14 @@ export type RunResult = (
|
|
|
20
20
|
| { readonly status: 'cancelled' }
|
|
21
21
|
) & { readonly steps: number; readonly trace: readonly ToolCall[]; readonly task?: string }
|
|
22
22
|
|
|
23
|
+
/** What a caller may pass to one run. */
|
|
24
|
+
export interface RunOptions {
|
|
25
|
+
/** Aborting it stops the run: the model call or tool in flight is dropped and the run ends `{ status: 'cancelled' }`. */
|
|
26
|
+
readonly signal?: AbortSignal
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
export interface Agent {
|
|
24
|
-
run(question: string): Promise<RunResult>
|
|
30
|
+
run(question: string, options?: RunOptions): Promise<RunResult>
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/** The step budget of one run, provided inside the run's own fiber. */
|
|
@@ -62,6 +68,19 @@ declare module '@deepseek-ai/cordis' {
|
|
|
62
68
|
|
|
63
69
|
const message = (error: unknown) => (error instanceof Error ? error.message : String(error))
|
|
64
70
|
|
|
71
|
+
/** Thrown inside a run when its signal aborts; the run turns it into `cancelled`. */
|
|
72
|
+
class Cancelled extends Error {}
|
|
73
|
+
|
|
74
|
+
/** The promise, or a rejection with `Cancelled` as soon as the signal aborts (a model or tool that ignores the signal cannot hold the run). */
|
|
75
|
+
function until<T>(work: Promise<T>, signal: AbortSignal): Promise<T> {
|
|
76
|
+
if (signal.aborted) return Promise.reject(new Cancelled())
|
|
77
|
+
return new Promise<T>((resolve, reject) => {
|
|
78
|
+
const onAbort = () => reject(new Cancelled())
|
|
79
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
80
|
+
work.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort))
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
/**
|
|
66
85
|
* The agent. Every run is its own child fiber that holds the run's budget and is disposed when the run ends, however it ends: a
|
|
67
86
|
* scope per task, written with plain Cordis plugins. A tool that throws does not end the run: the model sees the error as the result.
|
|
@@ -74,11 +93,15 @@ export const agent = {
|
|
|
74
93
|
const limit = config?.steps ?? 6
|
|
75
94
|
let runs = 0
|
|
76
95
|
ctx.provide('agent', {
|
|
77
|
-
async run(question) {
|
|
96
|
+
async run(question, options) {
|
|
78
97
|
runs += 1
|
|
79
98
|
const trace: ToolCall[] = []
|
|
80
99
|
const recorder = (ctx.get as (name: string) => unknown)('tasks') as TaskRecorder | undefined
|
|
81
100
|
const record = recorder?.start('agent-run', { question })
|
|
101
|
+
// The run stops when its task is cancelled or the caller's signal aborts.
|
|
102
|
+
const signal = AbortSignal.any(
|
|
103
|
+
[record?.signal, options?.signal].filter((s): s is AbortSignal => s !== undefined),
|
|
104
|
+
)
|
|
82
105
|
// Assigned inside the run's fiber; `as` keeps TypeScript from narrowing it to this first value.
|
|
83
106
|
let result = { status: 'budget_exceeded', steps: 0, trace } as RunResult
|
|
84
107
|
const task = ctx.plugin({
|
|
@@ -89,17 +112,28 @@ export const agent = {
|
|
|
89
112
|
const budget = scope.budget
|
|
90
113
|
let toolResult: unknown
|
|
91
114
|
while (budget.steps < budget.limit) {
|
|
92
|
-
if (
|
|
115
|
+
if (signal.aborted) {
|
|
116
|
+
result = { status: 'cancelled', steps: budget.steps, trace }
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
let step: Step
|
|
120
|
+
try {
|
|
121
|
+
step = await until(
|
|
122
|
+
scope.model.complete({
|
|
123
|
+
question,
|
|
124
|
+
step: budget.steps,
|
|
125
|
+
toolResult,
|
|
126
|
+
trace: [...trace],
|
|
127
|
+
tools: scope.tools.list(),
|
|
128
|
+
signal,
|
|
129
|
+
}),
|
|
130
|
+
signal,
|
|
131
|
+
)
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (!(error instanceof Cancelled)) throw error
|
|
93
134
|
result = { status: 'cancelled', steps: budget.steps, trace }
|
|
94
135
|
return
|
|
95
136
|
}
|
|
96
|
-
const step = await scope.model.complete({
|
|
97
|
-
question,
|
|
98
|
-
step: budget.steps,
|
|
99
|
-
toolResult,
|
|
100
|
-
trace: [...trace],
|
|
101
|
-
tools: scope.tools.list(),
|
|
102
|
-
})
|
|
103
137
|
budget.steps += 1
|
|
104
138
|
if (step.tool === undefined) {
|
|
105
139
|
scope.memory.remember(`${question} => ${step.text}`)
|
|
@@ -110,7 +144,10 @@ export const agent = {
|
|
|
110
144
|
? recorder?.start(`tool:${step.tool}`, step.input, { parent: record.id })
|
|
111
145
|
: undefined
|
|
112
146
|
try {
|
|
113
|
-
toolResult = await
|
|
147
|
+
toolResult = await until(
|
|
148
|
+
scope.tools.call(step.tool, step.input, { signal }),
|
|
149
|
+
signal,
|
|
150
|
+
)
|
|
114
151
|
trace.push({
|
|
115
152
|
tool: step.tool,
|
|
116
153
|
input: step.input,
|
|
@@ -119,6 +156,12 @@ export const agent = {
|
|
|
119
156
|
})
|
|
120
157
|
call?.complete(toolResult)
|
|
121
158
|
} catch (error) {
|
|
159
|
+
if (error instanceof Cancelled) {
|
|
160
|
+
trace.push({ tool: step.tool, input: step.input, error: 'cancelled' })
|
|
161
|
+
call?.fail('the run was cancelled')
|
|
162
|
+
result = { status: 'cancelled', steps: budget.steps, trace }
|
|
163
|
+
return
|
|
164
|
+
}
|
|
122
165
|
toolResult = { error: message(error) }
|
|
123
166
|
trace.push({
|
|
124
167
|
tool: step.tool,
|
|
@@ -143,7 +186,10 @@ export const agent = {
|
|
|
143
186
|
}
|
|
144
187
|
if (result.status === 'completed')
|
|
145
188
|
record?.complete(result.output, { steps: result.steps, trace })
|
|
146
|
-
else if (result.status === '
|
|
189
|
+
else if (result.status === 'cancelled') {
|
|
190
|
+
if (!record?.signal.aborted)
|
|
191
|
+
record?.fail('the run was cancelled', { steps: result.steps, trace })
|
|
192
|
+
} else if (result.status === 'budget_exceeded')
|
|
147
193
|
record?.fail('the run used its whole step budget', { steps: result.steps, trace })
|
|
148
194
|
return record ? { ...result, task: record.id } : result
|
|
149
195
|
},
|
package/plugins/agent/model.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface ModelInput {
|
|
|
22
22
|
}>
|
|
23
23
|
/** The tools the model may call. */
|
|
24
24
|
readonly tools?: ReadonlyArray<{ readonly name: string; readonly description: string }>
|
|
25
|
+
/** Aborts when the run is cancelled: a provider that can stop its request passes this on. */
|
|
26
|
+
readonly signal?: AbortSignal
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** What the agent needs from a model. Replace this plugin with a real provider when you have one. */
|
package/plugins/agent/tools.ts
CHANGED
|
@@ -3,7 +3,8 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
3
3
|
/** One tool: what it does (for the model) and how to run it. */
|
|
4
4
|
export interface Tool {
|
|
5
5
|
readonly description: string
|
|
6
|
-
run
|
|
6
|
+
/** `options.signal` aborts when the run is cancelled; a tool that can stop its work listens to it. */
|
|
7
|
+
run(input: unknown, options?: { signal?: AbortSignal }): unknown | Promise<unknown>
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
/** The tool registry. A plugin registers its tools inside `ctx.effect`, so they leave with it. */
|
|
@@ -11,7 +12,7 @@ export interface Tools {
|
|
|
11
12
|
/** Register a tool. Returns the function that removes it. */
|
|
12
13
|
register(name: string, tool: Tool): () => void
|
|
13
14
|
list(): Array<{ name: string; description: string }>
|
|
14
|
-
call(name: string, input: unknown): Promise<unknown>
|
|
15
|
+
call(name: string, input: unknown, options?: { signal?: AbortSignal }): Promise<unknown>
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -31,10 +32,10 @@ export function tools(ctx: Context) {
|
|
|
31
32
|
},
|
|
32
33
|
list: () =>
|
|
33
34
|
[...table.entries()].map(([name, tool]) => ({ name, description: tool.description })),
|
|
34
|
-
async call(name, input) {
|
|
35
|
+
async call(name, input, options) {
|
|
35
36
|
const tool = table.get(name)
|
|
36
37
|
if (!tool) throw new Error(`unknown tool "${name}"`)
|
|
37
|
-
return await tool.run(input)
|
|
38
|
+
return await tool.run(input, options)
|
|
38
39
|
},
|
|
39
40
|
}
|
|
40
41
|
ctx.provide('tools', registry)
|
|
@@ -177,7 +177,10 @@ export function apply(ctx: Context, config: PanelConfig = {}): void {
|
|
|
177
177
|
audit({ ask: 'start', driver, characters: message.length })
|
|
178
178
|
try {
|
|
179
179
|
if (driver === 'agent') {
|
|
180
|
-
|
|
180
|
+
// Stop in the page closes the stream; that cancels the run (the model call or tool in flight is dropped).
|
|
181
|
+
const cancel = new AbortController()
|
|
182
|
+
stream.onAbort(() => cancel.abort())
|
|
183
|
+
const result = await ctx.agent.run(message, { signal: cancel.signal })
|
|
181
184
|
for (const call of result.trace)
|
|
182
185
|
await send('tool', {
|
|
183
186
|
tool: call.tool,
|