@moxxy/sdk 0.43.0 → 0.45.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/agent-account-limit.d.ts +41 -0
- package/dist/agent-account-limit.d.ts.map +1 -0
- package/dist/agent-account-limit.js +64 -0
- package/dist/agent-account-limit.js.map +1 -0
- package/dist/agent-computer-use.d.ts +10 -0
- package/dist/agent-computer-use.d.ts.map +1 -0
- package/dist/agent-computer-use.js +24 -0
- package/dist/agent-computer-use.js.map +1 -0
- package/dist/agent-context-window.d.ts +22 -0
- package/dist/agent-context-window.d.ts.map +1 -0
- package/dist/agent-context-window.js +40 -0
- package/dist/agent-context-window.js.map +1 -0
- package/dist/agent-no-reply.d.ts +11 -0
- package/dist/agent-no-reply.d.ts.map +1 -0
- package/dist/agent-no-reply.js +13 -0
- package/dist/agent-no-reply.js.map +1 -0
- package/dist/agent-step.d.ts +14 -0
- package/dist/agent-step.d.ts.map +1 -0
- package/dist/agent-step.js +26 -0
- package/dist/agent-step.js.map +1 -0
- package/dist/agent-work.d.ts +34 -0
- package/dist/agent-work.d.ts.map +1 -0
- package/dist/agent-work.js +80 -0
- package/dist/agent-work.js.map +1 -0
- package/dist/attachment-payload.d.ts +95 -0
- package/dist/attachment-payload.d.ts.map +1 -0
- package/dist/attachment-payload.js +155 -0
- package/dist/attachment-payload.js.map +1 -0
- package/dist/auto-approve.d.ts +9 -0
- package/dist/auto-approve.d.ts.map +1 -1
- package/dist/auto-approve.js +18 -0
- package/dist/auto-approve.js.map +1 -1
- package/dist/channel-auth.d.ts +3 -3
- package/dist/channel-auth.d.ts.map +1 -1
- package/dist/channel-auth.js +1 -1
- package/dist/channel.d.ts +1 -1
- package/dist/client-session.d.ts +9 -5
- package/dist/client-session.d.ts.map +1 -1
- package/dist/computer-control.d.ts +11 -11
- package/dist/computer-control.d.ts.map +1 -1
- package/dist/computer-control.js +1 -1
- package/dist/computer-control.js.map +1 -1
- package/dist/conversation-history.d.ts +50 -0
- package/dist/conversation-history.d.ts.map +1 -0
- package/dist/conversation-history.js +227 -0
- package/dist/conversation-history.js.map +1 -0
- package/dist/cron.d.ts +58 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +320 -0
- package/dist/cron.js.map +1 -0
- package/dist/define.d.ts +2 -1
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +1 -0
- package/dist/define.js.map +1 -1
- package/dist/event-store.d.ts +1 -1
- package/dist/event-store.d.ts.map +1 -1
- package/dist/event-store.js +1 -0
- package/dist/event-store.js.map +1 -1
- package/dist/events.d.ts +23 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/file-diff.d.ts +31 -0
- package/dist/file-diff.d.ts.map +1 -0
- package/dist/file-diff.js +196 -0
- package/dist/file-diff.js.map +1 -0
- package/dist/index.d.ts +23 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -4
- package/dist/index.js.map +1 -1
- package/dist/mode/agent-step-loop.d.ts +13 -0
- package/dist/mode/agent-step-loop.d.ts.map +1 -0
- package/dist/mode/agent-step-loop.js +578 -0
- package/dist/mode/agent-step-loop.js.map +1 -0
- package/dist/mode/checkpoint-gate.d.ts +25 -0
- package/dist/mode/checkpoint-gate.d.ts.map +1 -0
- package/dist/mode/checkpoint-gate.js +150 -0
- package/dist/mode/checkpoint-gate.js.map +1 -0
- package/dist/mode/checkpoint.d.ts +6 -0
- package/dist/mode/checkpoint.d.ts.map +1 -1
- package/dist/mode/collect-stream.d.ts +2 -0
- package/dist/mode/collect-stream.d.ts.map +1 -1
- package/dist/mode/collect-stream.js +1 -1
- package/dist/mode/collect-stream.js.map +1 -1
- package/dist/mode/project-messages.d.ts +3 -1
- package/dist/mode/project-messages.d.ts.map +1 -1
- package/dist/mode/project-messages.js +122 -81
- package/dist/mode/project-messages.js.map +1 -1
- package/dist/mode/react-loop-options.d.ts +113 -0
- package/dist/mode/react-loop-options.d.ts.map +1 -0
- package/dist/mode/react-loop-options.js +2 -0
- package/dist/mode/react-loop-options.js.map +1 -0
- package/dist/mode/react-loop.d.ts +2 -99
- package/dist/mode/react-loop.d.ts.map +1 -1
- package/dist/mode/react-loop.js +7 -145
- package/dist/mode/react-loop.js.map +1 -1
- package/dist/mode.d.ts +19 -7
- package/dist/mode.d.ts.map +1 -1
- package/dist/mode.js +1 -1
- package/dist/mode.js.map +1 -1
- package/dist/npm-registry.d.ts +22 -0
- package/dist/npm-registry.d.ts.map +1 -0
- package/dist/npm-registry.js +54 -0
- package/dist/npm-registry.js.map +1 -0
- package/dist/permission.d.ts +31 -0
- package/dist/permission.d.ts.map +1 -1
- package/dist/process-invocation.d.ts +2 -0
- package/dist/process-invocation.d.ts.map +1 -1
- package/dist/process-invocation.js +2 -2
- package/dist/process-invocation.js.map +1 -1
- package/dist/process-tree.d.ts +9 -0
- package/dist/process-tree.d.ts.map +1 -0
- package/dist/process-tree.js +40 -0
- package/dist/process-tree.js.map +1 -0
- package/dist/provider-login-bridge.js +1 -1
- package/dist/provider-login-bridge.js.map +1 -1
- package/dist/provider.d.ts +408 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/question-draft.d.ts +19 -0
- package/dist/question-draft.d.ts.map +1 -0
- package/dist/question-draft.js +30 -0
- package/dist/question-draft.js.map +1 -0
- package/dist/question.d.ts +204 -0
- package/dist/question.d.ts.map +1 -0
- package/dist/question.js +141 -0
- package/dist/question.js.map +1 -0
- package/dist/questions.d.ts +7 -0
- package/dist/questions.d.ts.map +1 -0
- package/dist/questions.js +7 -0
- package/dist/questions.js.map +1 -0
- package/dist/resolvers.d.ts +14 -1
- package/dist/resolvers.d.ts.map +1 -1
- package/dist/resolvers.js +33 -21
- package/dist/resolvers.js.map +1 -1
- package/dist/schemas.d.ts +53 -53
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -2
- package/dist/schemas.js.map +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -1
- package/dist/session-like.d.ts +19 -1
- package/dist/session-like.d.ts.map +1 -1
- package/dist/starting-model.d.ts +12 -0
- package/dist/starting-model.d.ts.map +1 -0
- package/dist/starting-model.js +9 -0
- package/dist/starting-model.js.map +1 -0
- package/dist/tool-dispatch.d.ts.map +1 -1
- package/dist/tool-dispatch.js +10 -4
- package/dist/tool-dispatch.js.map +1 -1
- package/dist/tool-display.d.ts +79 -3
- package/dist/tool-display.d.ts.map +1 -1
- package/dist/tool-display.js +117 -3
- package/dist/tool-display.js.map +1 -1
- package/dist/tool-modes.d.ts +1 -1
- package/dist/tool-modes.d.ts.map +1 -1
- package/dist/tool-modes.js +8 -2
- package/dist/tool-modes.js.map +1 -1
- package/dist/tool-output.d.ts +15 -0
- package/dist/tool-output.d.ts.map +1 -0
- package/dist/tool-output.js +39 -0
- package/dist/tool-output.js.map +1 -0
- package/dist/tool.d.ts +14 -1
- package/dist/tool.d.ts.map +1 -1
- package/dist/tunnel.d.ts +3 -3
- package/dist/tunnel.d.ts.map +1 -1
- package/dist/tunnel.js +2 -2
- package/dist/workflow-approval.d.ts +9 -9
- package/dist/workflow-approval.d.ts.map +1 -1
- package/dist/workflow-approval.js +1 -1
- package/dist/workflow-approval.js.map +1 -1
- package/package.json +37 -5
- package/src/agent-account-limit.test.ts +87 -0
- package/src/agent-account-limit.ts +89 -0
- package/src/agent-computer-use.test.ts +26 -0
- package/src/agent-computer-use.ts +29 -0
- package/src/agent-context-window.ts +51 -0
- package/src/agent-no-reply.ts +16 -0
- package/src/agent-step.ts +26 -0
- package/src/agent-work.ts +102 -0
- package/src/attachment-payload.test.ts +165 -0
- package/src/attachment-payload.ts +187 -0
- package/src/auto-approve.test.ts +13 -1
- package/src/auto-approve.ts +19 -0
- package/src/channel-auth.ts +3 -3
- package/src/channel.ts +1 -1
- package/src/client-session.ts +9 -2
- package/src/computer-control.ts +1 -1
- package/src/conversation-history.test.ts +184 -0
- package/src/conversation-history.ts +272 -0
- package/src/cron.test.ts +280 -0
- package/src/cron.ts +355 -0
- package/src/define.test.ts +1 -1
- package/src/define.ts +3 -1
- package/src/event-store.ts +2 -1
- package/src/events.ts +23 -1
- package/src/file-diff.test.ts +103 -0
- package/src/file-diff.ts +208 -0
- package/src/index.ts +140 -1
- package/src/json-file-store.test.ts +1 -1
- package/src/loop-helpers.test.ts +15 -0
- package/src/mode/agent-step-loop.ts +645 -0
- package/src/mode/checkpoint-gate.ts +198 -0
- package/src/mode/checkpoint.ts +6 -0
- package/src/mode/collect-stream.ts +1 -1
- package/src/mode/project-messages.test.ts +118 -1
- package/src/mode/project-messages.ts +136 -77
- package/src/mode/react-loop-options.ts +124 -0
- package/src/mode/react-loop.ts +10 -296
- package/src/mode.ts +19 -7
- package/src/npm-registry.test.ts +95 -0
- package/src/npm-registry.ts +57 -0
- package/src/permission.ts +30 -0
- package/src/process-invocation.test.ts +25 -0
- package/src/process-invocation.ts +8 -3
- package/src/process-tree.ts +37 -0
- package/src/provider-login-bridge.ts +1 -1
- package/src/provider-schema-constraints.test.ts +1 -1
- package/src/provider-utils.test.ts +1 -1
- package/src/provider.ts +346 -0
- package/src/question-draft.test.ts +57 -0
- package/src/question-draft.ts +47 -0
- package/src/question.test.ts +130 -0
- package/src/question.ts +229 -0
- package/src/questions.ts +6 -0
- package/src/resolvers.test.ts +84 -1
- package/src/resolvers.ts +40 -27
- package/src/schemas.test.ts +5 -0
- package/src/schemas.ts +3 -2
- package/src/server.ts +3 -0
- package/src/session-like.ts +19 -1
- package/src/skill-mentions.test.ts +1 -1
- package/src/starting-model.test.ts +19 -0
- package/src/starting-model.ts +11 -0
- package/src/token-efficiency.test.ts +1 -1
- package/src/tool-dispatch.ts +10 -4
- package/src/tool-display.test.ts +119 -0
- package/src/tool-display.ts +162 -4
- package/src/tool-gating.test.ts +1 -1
- package/src/tool-icon.test.ts +1 -1
- package/src/tool-modes.test.ts +18 -1
- package/src/tool-modes.ts +6 -2
- package/src/tool-output.test.ts +46 -0
- package/src/tool-output.ts +46 -0
- package/src/tool.ts +14 -1
- package/src/tunnel.ts +3 -3
- package/src/workflow-approval.ts +1 -1
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the npm registry what version a package's `latest` dist-tag points at.
|
|
3
|
+
* Pure transport over global `fetch` — no `npm` dependency, no child process —
|
|
4
|
+
* so `moxxy update`, the TUI's "new version" banner and the agent CLI checks
|
|
5
|
+
* share one cheap call.
|
|
6
|
+
*
|
|
7
|
+
* Fail-soft by contract: every error (offline, timeout, 404, malformed body)
|
|
8
|
+
* resolves to `null`, never throws. A version check must never break a caller.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const REGISTRY = 'https://registry.npmjs.org';
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 4000;
|
|
13
|
+
const STABLE_SEMVER_RE = /^(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})$/;
|
|
14
|
+
|
|
15
|
+
function normalizeStableSemver(value: unknown): string | null {
|
|
16
|
+
if (typeof value !== 'string') return null;
|
|
17
|
+
const match = STABLE_SEMVER_RE.exec(value);
|
|
18
|
+
if (!match) return null;
|
|
19
|
+
const [, major, minor, patch] = match;
|
|
20
|
+
if (major === undefined || minor === undefined || patch === undefined) return null;
|
|
21
|
+
return `${Number(major)}.${Number(minor)}.${Number(patch)}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface FetchNpmLatestOptions {
|
|
25
|
+
/** Override the fetch implementation (tests inject a stub). */
|
|
26
|
+
fetchImpl?: typeof fetch;
|
|
27
|
+
/** Abort the request after this many ms (default 4000). */
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The version under the `latest` dist-tag for `pkg`, or `null` if it can't be
|
|
33
|
+
* resolved. Uses the per-tag manifest endpoint
|
|
34
|
+
* (`/<pkg>/latest`) so we transfer a tiny document, not the full packument.
|
|
35
|
+
*/
|
|
36
|
+
export async function fetchNpmLatest(pkg: string, opts: FetchNpmLatestOptions = {}): Promise<string | null> {
|
|
37
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
38
|
+
// Encode the whole package name (incl. the scope slash) so an arbitrary or
|
|
39
|
+
// attacker-influenced `pkg` can't inject path traversal / a query string /
|
|
40
|
+
// another host segment into the registry URL. npm accepts `@scope%2Fname`.
|
|
41
|
+
const url = `${REGISTRY}/${encodeURIComponent(pkg)}/latest`;
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetchImpl(url, {
|
|
46
|
+
signal: controller.signal,
|
|
47
|
+
headers: { accept: 'application/json' },
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) return null;
|
|
50
|
+
const json = (await res.json()) as { version?: unknown };
|
|
51
|
+
return normalizeStableSemver(json.version);
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
} finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/permission.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface PermissionRule {
|
|
|
30
30
|
}>;
|
|
31
31
|
};
|
|
32
32
|
readonly reason?: string;
|
|
33
|
+
/**
|
|
34
|
+
* How far an "allow for this session" answer reaches for this tool: only
|
|
35
|
+
* later calls with the same values under these input keys. For a tool whose
|
|
36
|
+
* approval grants something by itself (access to an app), where a grant of
|
|
37
|
+
* the whole tool would let every later, wider request through unasked.
|
|
38
|
+
*/
|
|
39
|
+
readonly sessionScope?: { readonly inputKeys: ReadonlyArray<string> };
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
export interface PendingToolCall {
|
|
@@ -44,9 +51,32 @@ export interface PendingToolCall {
|
|
|
44
51
|
readonly requestedAtSeq?: number;
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
/**
|
|
55
|
+
* A standing "allow" rule a channel saves to the person's permissions.json:
|
|
56
|
+
* the whole tool, or with `inputEquals` only calls whose inputs have exactly
|
|
57
|
+
* these values (`null` for a left-out one).
|
|
58
|
+
*/
|
|
59
|
+
export interface StandingAllowRule {
|
|
60
|
+
readonly name: string;
|
|
61
|
+
readonly reason?: string;
|
|
62
|
+
readonly inputEquals?: Readonly<Record<string, unknown>>;
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
export interface PermissionContext {
|
|
48
66
|
readonly turnId?: string;
|
|
49
67
|
readonly toolDescription?: string;
|
|
68
|
+
/**
|
|
69
|
+
* The rule declared by the tool this turn offers under the call's name. A
|
|
70
|
+
* mode may offer tools the session registry does not hold (plan mode's
|
|
71
|
+
* `plan_complete`), so the resolver needs it from the call site.
|
|
72
|
+
*/
|
|
73
|
+
readonly toolPermission?: PermissionRule;
|
|
74
|
+
/**
|
|
75
|
+
* The person answers even while auto-approve is on (standing rules still
|
|
76
|
+
* decide first): the agent's own setting would have asked them, and they
|
|
77
|
+
* never chose auto-approve for it.
|
|
78
|
+
*/
|
|
79
|
+
readonly askPerson?: boolean;
|
|
50
80
|
readonly skillContext?: string;
|
|
51
81
|
readonly sessionId: string;
|
|
52
82
|
}
|
|
@@ -272,4 +272,29 @@ describe('spawnExecutableTarget', () => {
|
|
|
272
272
|
|
|
273
273
|
expect(JSON.parse(readFileSync(observed, 'utf8'))).toEqual(args.slice(1));
|
|
274
274
|
});
|
|
275
|
+
|
|
276
|
+
it("gives a native program the environment its target carries, over the caller's", () => {
|
|
277
|
+
const child = spawnExecutableTarget(
|
|
278
|
+
{ kind: 'direct', command: process.execPath, env: { MOXXY_TARGET_ENV: 'from target' } },
|
|
279
|
+
['-e', 'process.stdout.write(String(process.env.MOXXY_TARGET_ENV))'],
|
|
280
|
+
{ stdio: ['ignore', 'pipe', 'pipe'], env: { MOXXY_TARGET_ENV: 'from caller' } },
|
|
281
|
+
);
|
|
282
|
+
let stdout = '';
|
|
283
|
+
child.stdout?.setEncoding('utf8');
|
|
284
|
+
child.stdout?.on('data', (chunk: string) => {
|
|
285
|
+
stdout += chunk;
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return new Promise<void>((resolve, reject) => {
|
|
289
|
+
child.once('error', reject);
|
|
290
|
+
child.once('close', () => {
|
|
291
|
+
try {
|
|
292
|
+
expect(stdout).toBe('from target');
|
|
293
|
+
resolve();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
reject(error);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
275
300
|
});
|
|
@@ -16,7 +16,12 @@ const KNOWN_NODE_CLIS = new Set<KnownNodeCli>(['moxxy', 'npm', 'pnpm', 'yarn']);
|
|
|
16
16
|
type KnownNodeCli = 'moxxy' | 'npm' | 'pnpm' | 'yarn';
|
|
17
17
|
|
|
18
18
|
export type ExecutableTarget =
|
|
19
|
-
| {
|
|
19
|
+
| {
|
|
20
|
+
readonly kind: 'direct';
|
|
21
|
+
readonly command: string;
|
|
22
|
+
/** Environment the program needs, set over the caller's. */
|
|
23
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
24
|
+
}
|
|
20
25
|
| {
|
|
21
26
|
readonly kind: 'node';
|
|
22
27
|
readonly command: string;
|
|
@@ -155,7 +160,7 @@ export function spawnExecutableTarget(
|
|
|
155
160
|
opts: SafeSpawnOptions = {},
|
|
156
161
|
): ChildProcess {
|
|
157
162
|
const argv = target.kind === 'node' ? [target.entry, ...args] : [...args];
|
|
158
|
-
const targetEnv = target.
|
|
163
|
+
const targetEnv = target.env ?? {};
|
|
159
164
|
return spawn(target.command, argv, {
|
|
160
165
|
...opts,
|
|
161
166
|
env: { ...process.env, ...opts.env, ...targetEnv },
|
|
@@ -170,7 +175,7 @@ export function execExecutableTargetSync(
|
|
|
170
175
|
opts: SafeExecFileSyncOptions = {},
|
|
171
176
|
): void {
|
|
172
177
|
const argv = target.kind === 'node' ? [target.entry, ...args] : [...args];
|
|
173
|
-
const targetEnv = target.
|
|
178
|
+
const targetEnv = target.env ?? {};
|
|
174
179
|
execFileSync(target.command, argv, {
|
|
175
180
|
...opts,
|
|
176
181
|
env: { ...process.env, ...opts.env, ...targetEnv },
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Signal the child's whole process group, not just the shell. The child must
|
|
6
|
+
* be spawned `detached` on POSIX so it leads its own group; a negative-pid kill
|
|
7
|
+
* then reaches every descendant (`sh -c 'sleep 1000 & wait'`, build workers,
|
|
8
|
+
* …) instead of orphaning them.
|
|
9
|
+
*/
|
|
10
|
+
export function killTree(child: ChildProcess, signal: NodeJS.Signals): void {
|
|
11
|
+
if (child.pid === undefined) return;
|
|
12
|
+
if (process.platform === 'win32') {
|
|
13
|
+
// Windows has no process groups; taskkill /T walks the tree down from the
|
|
14
|
+
// shell. Always forced: a console program has no SIGTERM handler to run.
|
|
15
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
16
|
+
const taskkill = path.win32.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe');
|
|
17
|
+
spawn(taskkill, ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true }).on('error', () => {
|
|
18
|
+
child.kill(signal);
|
|
19
|
+
});
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
// Deliberately *no* "child already exited" guard here: the group outlives
|
|
24
|
+
// its leader (SIGTERM may kill the shell while a TERM-ignoring descendant
|
|
25
|
+
// lives on), and the pgid stays valid — and un-reusable — while any
|
|
26
|
+
// member survives. ESRCH below covers the fully-gone case.
|
|
27
|
+
process.kill(-child.pid, signal);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
// ESRCH: the group is already gone — nothing to do. Anything else
|
|
30
|
+
// (e.g. EPERM, or the child somehow not leading a group): fall back to
|
|
31
|
+
// signalling the shell itself so we at least keep the old behavior.
|
|
32
|
+
if ((e as NodeJS.ErrnoException).code !== 'ESRCH') {
|
|
33
|
+
if (child.exitCode === null && child.signalCode === null) child.kill(signal);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { z } from 'zod';
|
|
2
|
+
import { z } from 'zod/v3';
|
|
3
3
|
import { zodToJsonSchema } from './provider-utils.js';
|
|
4
4
|
import { resolveProviderTools } from './provider-tool-utils.js';
|
|
5
5
|
import { defineTool } from './define.js';
|
package/src/provider.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { HostedTool, ToolDef } from './tool.js';
|
|
2
2
|
import type { ReasoningReplayItem } from './events.js';
|
|
3
|
+
import type { StopReason } from './provider-utils.js';
|
|
4
|
+
import type { QuestionAskOptions, QuestionItem, QuestionOutcome } from './question.js';
|
|
5
|
+
import type { AgentAccountLimit } from './agent-account-limit.js';
|
|
6
|
+
import type { Skill } from './skill.js';
|
|
3
7
|
|
|
4
8
|
export interface ProviderMessage {
|
|
5
9
|
readonly role: 'system' | 'user' | 'assistant' | 'tool_result';
|
|
@@ -173,6 +177,14 @@ export interface ModelDescriptor {
|
|
|
173
177
|
export interface LLMProvider {
|
|
174
178
|
readonly name: string;
|
|
175
179
|
readonly models: ReadonlyArray<ModelDescriptor>;
|
|
180
|
+
/**
|
|
181
|
+
* The model a new conversation starts on while the person chose no default
|
|
182
|
+
* for this provider: a light one, so an unset default does not spend their
|
|
183
|
+
* plan on the biggest model. Ignored when the provider does not list it.
|
|
184
|
+
*/
|
|
185
|
+
readonly starterModel?: string;
|
|
186
|
+
/** For an agent connection, the agent's name as the person knows it ("Codex"), the same as its `ProviderDef.agentName`. */
|
|
187
|
+
readonly agentName?: string;
|
|
176
188
|
/**
|
|
177
189
|
* Optional provider-owned metadata preparation for a dynamically selected
|
|
178
190
|
* model. The runtime awaits it before it calculates context budgets, so a
|
|
@@ -184,6 +196,41 @@ export interface LLMProvider {
|
|
|
184
196
|
options?: { readonly signal?: AbortSignal },
|
|
185
197
|
): Promise<void>;
|
|
186
198
|
stream(req: ProviderRequest): AsyncIterable<ProviderEvent>;
|
|
199
|
+
/**
|
|
200
|
+
* Present on an agent connection: run one turn of the vendor's own coding
|
|
201
|
+
* agent. The shared loop then calls this instead of {@link stream} for the
|
|
202
|
+
* conversation's turns; `stream` still serves one-shot text calls.
|
|
203
|
+
*/
|
|
204
|
+
agentStep?(req: AgentStepRequest): AsyncIterable<AgentStepEvent>;
|
|
205
|
+
/**
|
|
206
|
+
* On an agent connection: the plan and goal the vendor's agent keeps itself
|
|
207
|
+
* (Claude, Codex), handed back as `plan` and `goal` events. Moxxy's plan and
|
|
208
|
+
* goal modes steer an agent without them like any model: through
|
|
209
|
+
* `plan_complete` / `goal_complete` it calls over MCP, and a nudge when it
|
|
210
|
+
* stops without one.
|
|
211
|
+
*/
|
|
212
|
+
readonly agentOwns?: AgentOwnModes;
|
|
213
|
+
/**
|
|
214
|
+
* On an agent connection whose agent can start a turn by itself (Claude after
|
|
215
|
+
* a task it ran in the background finished): tell the listener when it does
|
|
216
|
+
* while no Moxxy turn is running, so the conversation opens a turn for it
|
|
217
|
+
* (`origin.kind: 'agent'`) whose `agentStep` reads the agent's turn
|
|
218
|
+
* (`continuesAgentTurn`). Returns the unsubscribe.
|
|
219
|
+
*/
|
|
220
|
+
onAgentTurn?(listener: (start: AgentTurnStart) => void): () => void;
|
|
221
|
+
/**
|
|
222
|
+
* On an agent connection: what the agent's background work does while no
|
|
223
|
+
* Moxxy turn runs (its helpers' steps, `background_work` as it changes), for
|
|
224
|
+
* the conversation to record at once rather than at its next turn. Returns
|
|
225
|
+
* the unsubscribe.
|
|
226
|
+
*/
|
|
227
|
+
onAgentActivity?(listener: (activity: AgentActivity) => void): () => void;
|
|
228
|
+
/**
|
|
229
|
+
* On an agent connection: stop one work the agent runs in the background in
|
|
230
|
+
* the conversation (`AgentBackgroundWork.id`). Resolves `false` when the
|
|
231
|
+
* agent has no such work running.
|
|
232
|
+
*/
|
|
233
|
+
stopBackgroundWork?(sessionId: string, workId: string): Promise<boolean>;
|
|
187
234
|
countTokens(req: Pick<ProviderRequest, 'model' | 'messages' | 'system' | 'tools'>): Promise<number>;
|
|
188
235
|
}
|
|
189
236
|
|
|
@@ -337,9 +384,74 @@ export interface ProviderOAuthStatus {
|
|
|
337
384
|
readonly message?: string;
|
|
338
385
|
}
|
|
339
386
|
|
|
387
|
+
/**
|
|
388
|
+
* Where an agent connection's CLI stands on this computer:
|
|
389
|
+
* - `missing`: not found, so the connection cannot work until it is installed;
|
|
390
|
+
* - `update-required`: older than the oldest version Moxxy was checked against;
|
|
391
|
+
* - `update-available`: works, and the vendor has published a newer one;
|
|
392
|
+
* - `current`: works, and nothing newer is known;
|
|
393
|
+
* - `unknown`: found, but its version could not be read.
|
|
394
|
+
*/
|
|
395
|
+
export type AgentCliState = 'missing' | 'update-required' | 'update-available' | 'current' | 'unknown';
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Where an agent CLI came from, by where its file really is: the vendor's
|
|
399
|
+
* installer, Homebrew, a global npm install, inside an app (which updates it
|
|
400
|
+
* itself), or none of these.
|
|
401
|
+
*/
|
|
402
|
+
export type AgentCliOrigin = 'installer' | 'homebrew' | 'npm' | 'app' | 'unknown';
|
|
403
|
+
|
|
404
|
+
/** The last check of an agent connection's CLI, as every surface shows it. */
|
|
405
|
+
export interface AgentCliReport {
|
|
406
|
+
readonly state: AgentCliState;
|
|
407
|
+
/** The CLI's command ("codex"), which also names it to `moxxy agents`. */
|
|
408
|
+
readonly command: string;
|
|
409
|
+
/** The version found on this computer; null when missing or unreadable. */
|
|
410
|
+
readonly installed: string | null;
|
|
411
|
+
/** The file that was checked, which a conversation also runs; absent when missing. */
|
|
412
|
+
readonly path?: string;
|
|
413
|
+
/** Where that file came from; absent when missing. */
|
|
414
|
+
readonly origin?: AgentCliOrigin;
|
|
415
|
+
/** The vendor's latest published version; null when the registry could not be asked. */
|
|
416
|
+
readonly latest: string | null;
|
|
417
|
+
/** The oldest version Moxxy works with. */
|
|
418
|
+
readonly minimum: string;
|
|
419
|
+
/** When the check ran, in epoch milliseconds. */
|
|
420
|
+
readonly checkedAt: number;
|
|
421
|
+
/** Whether the agent is signed in; absent when the CLI is missing or the connection does not say. */
|
|
422
|
+
readonly signedIn?: boolean;
|
|
423
|
+
/** Who it is signed in as, in the agent's words (Claude's e-mail, "ChatGPT" for Codex); null when it does not say. */
|
|
424
|
+
readonly account?: string | null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** How an agent connection's CLI is checked; the host runs it at start and every hour. */
|
|
428
|
+
export interface ProviderAgentCli {
|
|
429
|
+
/** `refresh` asks the vendor's registry again instead of trusting a recent answer. */
|
|
430
|
+
check(opts?: { readonly refresh?: boolean }): Promise<AgentCliReport>;
|
|
431
|
+
/**
|
|
432
|
+
* The models the agent offers the signed-in account now, as its own picker
|
|
433
|
+
* lists them. Asked after a check finds the agent installed and signed in;
|
|
434
|
+
* the connection's built-in `models` stay the answer until it succeeds.
|
|
435
|
+
*/
|
|
436
|
+
models?(): Promise<ReadonlyArray<ModelDescriptor>>;
|
|
437
|
+
}
|
|
438
|
+
|
|
340
439
|
export interface ProviderDef {
|
|
341
440
|
readonly name: string;
|
|
342
441
|
readonly models: ReadonlyArray<ModelDescriptor>;
|
|
442
|
+
/** What {@link LLMProvider.starterModel} says, known before the provider is built. */
|
|
443
|
+
readonly starterModel?: string;
|
|
444
|
+
/** For an agent connection, the agent's name as the person knows it ("Codex"), which surfaces use to say what it does. */
|
|
445
|
+
readonly agentName?: string;
|
|
446
|
+
/** For an agent connection, how its CLI is checked for presence and updates. */
|
|
447
|
+
readonly agentCli?: ProviderAgentCli;
|
|
448
|
+
/**
|
|
449
|
+
* For an agent connection, whether the agent as the person set it up acts
|
|
450
|
+
* without asking in `cwd` (`undefined` when it cannot tell). Read as a
|
|
451
|
+
* conversation starts on the connection, so its auto-approve is the truth
|
|
452
|
+
* from the first moment; a switch the conversation recorded always wins.
|
|
453
|
+
*/
|
|
454
|
+
ownAutoApprove?(opts: { readonly cwd: string }): Promise<boolean | undefined>;
|
|
343
455
|
/**
|
|
344
456
|
* True when the provider can return its current model catalog at runtime.
|
|
345
457
|
* Hosts use this capability signal to offer live discovery instead of
|
|
@@ -367,3 +479,237 @@ export interface ProviderDef {
|
|
|
367
479
|
*/
|
|
368
480
|
readonly auth?: ProviderAuthDescriptor;
|
|
369
481
|
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Agent connections: the vendor's own coding agent (Claude, Codex, OpenCode)
|
|
485
|
+
* does the turn, unmodified. A provider that runs one implements
|
|
486
|
+
* `LLMProvider.agentStep`; the shared loop then runs one agent turn instead of
|
|
487
|
+
* "call the model, run the tools, repeat" and records what the agent did as
|
|
488
|
+
* ordinary Moxxy events. Everything the agent needs from Moxxy (its tools, the
|
|
489
|
+
* person's approval, a Question) goes through {@link AgentStepHost}.
|
|
490
|
+
*/
|
|
491
|
+
|
|
492
|
+
/** One turn of the vendor's agent. */
|
|
493
|
+
export interface AgentStepRequest {
|
|
494
|
+
/** The Moxxy conversation, so an adapter keeps one live agent per conversation. */
|
|
495
|
+
readonly sessionId: string;
|
|
496
|
+
/** The model the person picked, exactly as the agent names it. */
|
|
497
|
+
readonly model: string;
|
|
498
|
+
/** The person's message for this turn (or the loop's follow-up), with its images and documents. */
|
|
499
|
+
readonly prompt: ReadonlyArray<ContentBlock>;
|
|
500
|
+
readonly cwd: string;
|
|
501
|
+
/** The active Moxxy mode (`default`, `plan`, `goal`, …), mapped by the adapter onto the agent's own. */
|
|
502
|
+
readonly mode: string;
|
|
503
|
+
/** Moxxy tools this mode offers; the adapter lends the ones the agent lacks over Moxxy's MCP server. */
|
|
504
|
+
readonly tools: ReadonlyArray<ToolDef>;
|
|
505
|
+
/**
|
|
506
|
+
* Every Moxxy tool of the conversation, in any mode. Agents read their tool
|
|
507
|
+
* list once (Codex as a thread opens, Claude as it starts), so the adapter
|
|
508
|
+
* lists these and refuses, with the reason, a call to one `tools` lacks.
|
|
509
|
+
*/
|
|
510
|
+
readonly allTools?: ReadonlyArray<ToolDef>;
|
|
511
|
+
/** The conversation's Moxxy skills now; the adapter offers the agent those meant for it, through Moxxy's MCP server. */
|
|
512
|
+
readonly skills?: ReadonlyArray<Skill>;
|
|
513
|
+
/** The agent's own session from an earlier turn of this conversation, to resume. */
|
|
514
|
+
readonly resumeSessionId?: string;
|
|
515
|
+
/**
|
|
516
|
+
* The conversation before this turn, from Moxxy's record, for an agent that
|
|
517
|
+
* starts a new session of its own while the conversation already has turns
|
|
518
|
+
* (its earlier session is gone, or it joins partway). `undefined` when no
|
|
519
|
+
* turn came before. Built only when called.
|
|
520
|
+
*/
|
|
521
|
+
readonly earlierConversation?: () => string | undefined;
|
|
522
|
+
/**
|
|
523
|
+
* The turns that ran on another connection since this agent's own last turn
|
|
524
|
+
* of the conversation, from Moxxy's record, for an agent that goes on in its
|
|
525
|
+
* own session: it was away for them. `undefined` when it missed none or
|
|
526
|
+
* never ran here. Built only when called.
|
|
527
|
+
*/
|
|
528
|
+
readonly missedTurns?: () => string | undefined;
|
|
529
|
+
/** The conversation's auto-approve, one state with the agent's approval policy. */
|
|
530
|
+
readonly autoApprove: boolean;
|
|
531
|
+
/**
|
|
532
|
+
* The conversation recorded a switch: the adapter keeps `autoApprove` as it
|
|
533
|
+
* is, never the agent's own setup over it. Until one is recorded, the agent
|
|
534
|
+
* runs and shows the way the person set it up.
|
|
535
|
+
*/
|
|
536
|
+
readonly autoApproveRecorded?: boolean;
|
|
537
|
+
/**
|
|
538
|
+
* The switch in force is the agent's own setup Moxxy took on, not the
|
|
539
|
+
* person's choice; an agent whose own setup still asks the person about
|
|
540
|
+
* something (OpenCode: work outside the project) keeps asking then.
|
|
541
|
+
*/
|
|
542
|
+
readonly autoApproveFromAgent?: boolean;
|
|
543
|
+
readonly reasoning?: { readonly effort?: ReasoningEffort } | boolean;
|
|
544
|
+
/** The person asked for fast mode; the adapter runs the agent on its own faster tier, where it has one. */
|
|
545
|
+
readonly fast?: boolean;
|
|
546
|
+
readonly signal: AbortSignal;
|
|
547
|
+
readonly host: AgentStepHost;
|
|
548
|
+
/**
|
|
549
|
+
* The turn is one the agent started by itself (see `onAgentTurn`): read the
|
|
550
|
+
* agent's turn through to its end and send it nothing; `prompt` is only the
|
|
551
|
+
* marker Moxxy recorded for it.
|
|
552
|
+
*/
|
|
553
|
+
readonly continuesAgentTurn?: true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/** The agent started a turn by itself in a Moxxy conversation. */
|
|
557
|
+
export interface AgentTurnStart {
|
|
558
|
+
/** The Moxxy conversation (`AgentStepRequest.sessionId`). */
|
|
559
|
+
readonly sessionId: string;
|
|
560
|
+
/** What the agent took up, for the marker that opens the turn ("List files finished in the background"). */
|
|
561
|
+
readonly name: string;
|
|
562
|
+
/** The background work whose end it took up (`AgentBackgroundWork.id`), when the agent says. */
|
|
563
|
+
readonly workId?: string;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** What Moxxy does for the agent during its turn. */
|
|
567
|
+
export interface AgentStepHost {
|
|
568
|
+
/** Run a Moxxy tool the agent called (over MCP) through Moxxy's hooks, permissions and log. */
|
|
569
|
+
runTool(call: AgentToolCall): Promise<AgentToolOutcome>;
|
|
570
|
+
/**
|
|
571
|
+
* Decide whether the agent may run one of its own tools. The person answers
|
|
572
|
+
* unless auto-approve or a standing rule does; with `policyOnly` only hooks
|
|
573
|
+
* and standing rules (`permissions.json`) are consulted, never the person;
|
|
574
|
+
* with `askPerson` the person answers even while auto-approve is on.
|
|
575
|
+
*/
|
|
576
|
+
approve(request: AgentApprovalRequest): Promise<AgentApprovalDecision>;
|
|
577
|
+
/** Show the agent's Question in every Moxxy window of the conversation. */
|
|
578
|
+
ask(questions: ReadonlyArray<QuestionItem>, opts?: QuestionAskOptions): Promise<QuestionOutcome>;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export interface AgentToolCall {
|
|
582
|
+
readonly callId: string;
|
|
583
|
+
readonly name: string;
|
|
584
|
+
readonly input: unknown;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export type AgentToolOutcome =
|
|
588
|
+
| { readonly ok: true; readonly output: unknown }
|
|
589
|
+
| {
|
|
590
|
+
readonly ok: false;
|
|
591
|
+
readonly message: string;
|
|
592
|
+
/** The person refused a tool whose refusal ends the turn: stop the agent's turn. */
|
|
593
|
+
readonly endsTurn?: boolean;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
export interface AgentApprovalRequest extends AgentToolCall {
|
|
597
|
+
readonly description?: string;
|
|
598
|
+
readonly policyOnly?: boolean;
|
|
599
|
+
/** The agent's own setting would have asked the person about this call, and auto-approve was not their choice. */
|
|
600
|
+
readonly askPerson?: boolean;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export type AgentApprovalDecision =
|
|
604
|
+
| { readonly allow: true; /** The person allowed it for the rest of the session. */ readonly always?: boolean }
|
|
605
|
+
| { readonly allow: false; readonly reason: string };
|
|
606
|
+
|
|
607
|
+
export type AgentGoalStatus = 'active' | 'complete';
|
|
608
|
+
|
|
609
|
+
export interface AgentOwnModes {
|
|
610
|
+
readonly plan?: boolean;
|
|
611
|
+
readonly goal?: boolean;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Work an agent connection's agent runs in the background: a command it left
|
|
616
|
+
* running, or a helper agent going on after the turn that started it.
|
|
617
|
+
*/
|
|
618
|
+
export interface AgentBackgroundWork {
|
|
619
|
+
/** The agent's id for it; a helper's is the `id` of its `subagent_*` events. */
|
|
620
|
+
readonly id: string;
|
|
621
|
+
readonly kind: 'command' | 'agent';
|
|
622
|
+
/** What it does, as the agent names it ("npm run dev", "inspect folder"). */
|
|
623
|
+
readonly label: string;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* A time the agent of an agent connection set itself to come back to the
|
|
628
|
+
* conversation by itself (Claude's ScheduleWakeup, `/loop`, CronCreate).
|
|
629
|
+
*/
|
|
630
|
+
export interface AgentScheduledWakeup {
|
|
631
|
+
/** The agent's id for it. */
|
|
632
|
+
readonly id: string;
|
|
633
|
+
/** What the agent is told when it comes back. */
|
|
634
|
+
readonly prompt: string;
|
|
635
|
+
/** When it comes back next (epoch ms); null when it cannot be told. */
|
|
636
|
+
readonly dueAtMs: number | null;
|
|
637
|
+
/** Comes back on every match of its schedule, not once. */
|
|
638
|
+
readonly recurring: boolean;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** How a background work ended. */
|
|
642
|
+
export type AgentWorkEnd = 'completed' | 'failed' | 'stopped';
|
|
643
|
+
|
|
644
|
+
/** What the agent's background work did while no Moxxy turn ran, for the conversation `sessionId`. */
|
|
645
|
+
export interface AgentActivity {
|
|
646
|
+
readonly sessionId: string;
|
|
647
|
+
readonly events: ReadonlyArray<AgentStepEvent>;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** What the agent did, in the order it did it. */
|
|
651
|
+
export type AgentStepEvent =
|
|
652
|
+
| { readonly type: 'text_delta'; readonly delta: string }
|
|
653
|
+
| { readonly type: 'reasoning_delta'; readonly delta: string }
|
|
654
|
+
/** A finished assistant message; `tool_use` when the agent went on to call tools. */
|
|
655
|
+
| { readonly type: 'message'; readonly text: string; readonly stopReason?: StopReason }
|
|
656
|
+
| { readonly type: 'reasoning'; readonly text: string }
|
|
657
|
+
/** A tool the agent runs itself (its Bash, Edit, …). */
|
|
658
|
+
| { readonly type: 'tool_call'; readonly callId: string; readonly name: string; readonly input: unknown }
|
|
659
|
+
| { readonly type: 'tool_result'; readonly callId: string; readonly ok: boolean; readonly output?: unknown; readonly error?: string }
|
|
660
|
+
/** The agent's own session id, kept to resume it. */
|
|
661
|
+
| { readonly type: 'session'; readonly sessionId: string }
|
|
662
|
+
/**
|
|
663
|
+
* The agent's real approval policy; it becomes the conversation's auto-approve.
|
|
664
|
+
* `elsewhere` when it changed without Moxxy (a hook, another client, the agent itself).
|
|
665
|
+
*/
|
|
666
|
+
| { readonly type: 'auto_approve'; readonly enabled: boolean; readonly elsewhere?: true }
|
|
667
|
+
/**
|
|
668
|
+
* Something outside Moxxy let the agent act without asking while auto-approve
|
|
669
|
+
* was off, and the connection set it back: only the person turns auto-approve on.
|
|
670
|
+
*/
|
|
671
|
+
| { readonly type: 'auto_approve_restored' }
|
|
672
|
+
/** The state of the agent's own goal after its turn: still open, or met. */
|
|
673
|
+
| { readonly type: 'goal'; readonly status: AgentGoalStatus }
|
|
674
|
+
/** The agent's own finished plan (Markdown), waiting for the person to carry it out. */
|
|
675
|
+
| { readonly type: 'plan'; readonly text: string }
|
|
676
|
+
/**
|
|
677
|
+
* A helper agent the agent started itself (Claude's Agent tool, a Codex subagent, OpenCode's task),
|
|
678
|
+
* its tool calls, and its end. `id` ties them together; it need not be a Moxxy session.
|
|
679
|
+
*/
|
|
680
|
+
/** `toolsUnseen` when the agent does not report the subagent's own tool calls (OpenCode). */
|
|
681
|
+
| { readonly type: 'subagent_started'; readonly id: string; readonly label: string; readonly agentType: string; readonly prompt?: string; readonly toolsUnseen?: true }
|
|
682
|
+
| { readonly type: 'subagent_tool_call'; readonly id: string; readonly name: string; readonly input: unknown }
|
|
683
|
+
/** What a subagent said as it worked, before its answer. */
|
|
684
|
+
| { readonly type: 'subagent_message'; readonly id: string; readonly text: string }
|
|
685
|
+
| {
|
|
686
|
+
readonly type: 'subagent_completed';
|
|
687
|
+
readonly id: string;
|
|
688
|
+
readonly text: string;
|
|
689
|
+
readonly tokensUsed?: number;
|
|
690
|
+
readonly error?: string;
|
|
691
|
+
/** It ended because it was stopped (by the person, or by the agent), not because it failed. */
|
|
692
|
+
readonly stopped?: true;
|
|
693
|
+
}
|
|
694
|
+
/** Everything the agent runs in the background now; it replaces the list before, so a missed end cannot leave work shown running. */
|
|
695
|
+
| { readonly type: 'background_work'; readonly work: ReadonlyArray<AgentBackgroundWork> }
|
|
696
|
+
/** Every time the agent set itself to come back; it replaces the list before. Not recorded in the log: it is what is planned now. */
|
|
697
|
+
| { readonly type: 'scheduled_wakeups'; readonly wakeups: ReadonlyArray<AgentScheduledWakeup> }
|
|
698
|
+
/** One background work ended; `output` is what a command printed last. */
|
|
699
|
+
| {
|
|
700
|
+
readonly type: 'background_work_ended';
|
|
701
|
+
readonly id: string;
|
|
702
|
+
readonly kind: AgentBackgroundWork['kind'];
|
|
703
|
+
readonly label: string;
|
|
704
|
+
readonly status: AgentWorkEnd;
|
|
705
|
+
readonly summary?: string;
|
|
706
|
+
readonly output?: string;
|
|
707
|
+
}
|
|
708
|
+
/** How much of the person's account the agent may still use; a reached limit ends the turn without a raw error. */
|
|
709
|
+
| { readonly type: 'account_limit'; readonly limit: AgentAccountLimit }
|
|
710
|
+
| { readonly type: 'usage'; readonly usage: TokenUsage }
|
|
711
|
+
/** How many tokens the agent's context holds now, when its usage is not per call (a whole turn's total). */
|
|
712
|
+
| { readonly type: 'context_used'; readonly tokens: number }
|
|
713
|
+
/** The context window the agent runs the model with, as it reports it after a response. */
|
|
714
|
+
| { readonly type: 'context_window'; readonly model: string; readonly tokens: number }
|
|
715
|
+
| { readonly type: 'error'; readonly message: string; readonly retryable: boolean };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { QuestionItem } from './question.js';
|
|
3
|
+
import { chooseOption, draftAnswers, emptyDraft, typeOwnAnswer } from './question-draft.js';
|
|
4
|
+
|
|
5
|
+
const colour: QuestionItem = {
|
|
6
|
+
id: 'q1',
|
|
7
|
+
question: 'Which colour?',
|
|
8
|
+
options: [
|
|
9
|
+
{ id: 'red', label: 'Red' },
|
|
10
|
+
{ id: 'blue', label: 'Blue' },
|
|
11
|
+
],
|
|
12
|
+
multiSelect: false,
|
|
13
|
+
allowOther: true,
|
|
14
|
+
};
|
|
15
|
+
const parts: QuestionItem = {
|
|
16
|
+
id: 'q2',
|
|
17
|
+
question: 'Which parts?',
|
|
18
|
+
options: [
|
|
19
|
+
{ id: 'auth', label: 'Auth' },
|
|
20
|
+
{ id: 'billing', label: 'Billing' },
|
|
21
|
+
],
|
|
22
|
+
multiSelect: true,
|
|
23
|
+
allowOther: true,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('answering in the card', () => {
|
|
27
|
+
it('takes one choice where only one fits, replacing the earlier one and any typed answer', () => {
|
|
28
|
+
let draft = typeOwnAnswer(emptyDraft([colour]), colour, 'Teal');
|
|
29
|
+
draft = chooseOption(draft, colour, 'red');
|
|
30
|
+
draft = chooseOption(draft, colour, 'blue');
|
|
31
|
+
|
|
32
|
+
expect(draftAnswers(draft)).toEqual({ q1: { selected: ['blue'] } });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('toggles choices where several fit, and keeps typed words beside them', () => {
|
|
36
|
+
let draft = emptyDraft([parts]);
|
|
37
|
+
draft = chooseOption(draft, parts, 'auth');
|
|
38
|
+
draft = chooseOption(draft, parts, 'billing');
|
|
39
|
+
draft = chooseOption(draft, parts, 'auth');
|
|
40
|
+
draft = typeOwnAnswer(draft, parts, 'Logs');
|
|
41
|
+
|
|
42
|
+
expect(draftAnswers(draft)).toEqual({ q2: { selected: ['billing'], other: 'Logs' } });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('lets typed words stand alone as the one answer, clearing the choice', () => {
|
|
46
|
+
let draft = chooseOption(emptyDraft([colour]), colour, 'red');
|
|
47
|
+
draft = typeOwnAnswer(draft, colour, 'Teal');
|
|
48
|
+
|
|
49
|
+
expect(draftAnswers(draft)).toEqual({ q1: { selected: [], other: 'Teal' } });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('sends no empty typed answer', () => {
|
|
53
|
+
const draft = typeOwnAnswer(chooseOption(emptyDraft([parts]), parts, 'auth'), parts, ' ');
|
|
54
|
+
|
|
55
|
+
expect(draftAnswers(draft)).toEqual({ q2: { selected: ['auth'] } });
|
|
56
|
+
});
|
|
57
|
+
});
|