@moxxy/sdk 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/dist/agent.d.ts +39 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +2 -0
- package/dist/agent.js.map +1 -0
- package/dist/cache-strategy.d.ts +35 -0
- package/dist/cache-strategy.d.ts.map +1 -0
- package/dist/cache-strategy.js +2 -0
- package/dist/cache-strategy.js.map +1 -0
- package/dist/channel.d.ts +146 -0
- package/dist/channel.d.ts.map +1 -0
- package/dist/channel.js +2 -0
- package/dist/channel.js.map +1 -0
- package/dist/client-session.d.ts +83 -0
- package/dist/client-session.d.ts.map +1 -0
- package/dist/client-session.js +2 -0
- package/dist/client-session.js.map +1 -0
- package/dist/command.d.ts +90 -0
- package/dist/command.d.ts.map +1 -0
- package/dist/command.js +15 -0
- package/dist/command.js.map +1 -0
- package/dist/compactor-helpers.d.ts +34 -0
- package/dist/compactor-helpers.d.ts.map +1 -0
- package/dist/compactor-helpers.js +156 -0
- package/dist/compactor-helpers.js.map +1 -0
- package/dist/compactor.d.ts +20 -0
- package/dist/compactor.d.ts.map +1 -0
- package/dist/compactor.js +2 -0
- package/dist/compactor.js.map +1 -0
- package/dist/define.d.ts +51 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +71 -0
- package/dist/define.js.map +1 -0
- package/dist/elision-helpers.d.ts +27 -0
- package/dist/elision-helpers.d.ts.map +1 -0
- package/dist/elision-helpers.js +109 -0
- package/dist/elision-helpers.js.map +1 -0
- package/dist/elision-state.d.ts +49 -0
- package/dist/elision-state.d.ts.map +1 -0
- package/dist/elision-state.js +144 -0
- package/dist/elision-state.js.map +1 -0
- package/dist/embedding-cache.d.ts +32 -0
- package/dist/embedding-cache.d.ts.map +1 -0
- package/dist/embedding-cache.js +0 -0
- package/dist/embedding-cache.js.map +1 -0
- package/dist/embedding.d.ts +21 -0
- package/dist/embedding.d.ts.map +1 -0
- package/dist/embedding.js +2 -0
- package/dist/embedding.js.map +1 -0
- package/dist/errors.d.ts +81 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +241 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +193 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +2 -0
- package/dist/events.js.map +1 -0
- package/dist/hooks.d.ts +52 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +2 -0
- package/dist/hooks.js.map +1 -0
- package/dist/ids.d.ts +18 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +7 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/install-hints.d.ts +26 -0
- package/dist/install-hints.d.ts.map +1 -0
- package/dist/install-hints.js +15 -0
- package/dist/install-hints.js.map +1 -0
- package/dist/isolation.d.ts +125 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +11 -0
- package/dist/isolation.js.map +1 -0
- package/dist/log.d.ts +11 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +2 -0
- package/dist/log.js.map +1 -0
- package/dist/mode-helpers.d.ts +99 -0
- package/dist/mode-helpers.d.ts.map +1 -0
- package/dist/mode-helpers.js +368 -0
- package/dist/mode-helpers.js.map +1 -0
- package/dist/mode.d.ts +133 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +2 -0
- package/dist/mode.js.map +1 -0
- package/dist/permission.d.ts +35 -0
- package/dist/permission.d.ts.map +1 -0
- package/dist/permission.js +2 -0
- package/dist/permission.js.map +1 -0
- package/dist/plugin.d.ts +88 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +2 -0
- package/dist/plugin.js.map +1 -0
- package/dist/provider-utils.d.ts +40 -0
- package/dist/provider-utils.d.ts.map +1 -0
- package/dist/provider-utils.js +163 -0
- package/dist/provider-utils.js.map +1 -0
- package/dist/provider.d.ts +219 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +2 -0
- package/dist/provider.js.map +1 -0
- package/dist/requirements.d.ts +22 -0
- package/dist/requirements.d.ts.map +1 -0
- package/dist/requirements.js +2 -0
- package/dist/requirements.js.map +1 -0
- package/dist/schemas.d.ts +228 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +88 -0
- package/dist/schemas.js.map +1 -0
- package/dist/session-like.d.ts +111 -0
- package/dist/session-like.d.ts.map +1 -0
- package/dist/session-like.js +2 -0
- package/dist/session-like.js.map +1 -0
- package/dist/skill.d.ts +30 -0
- package/dist/skill.d.ts.map +1 -0
- package/dist/skill.js +2 -0
- package/dist/skill.js.map +1 -0
- package/dist/subagent.d.ts +45 -0
- package/dist/subagent.d.ts.map +1 -0
- package/dist/subagent.js +11 -0
- package/dist/subagent.js.map +1 -0
- package/dist/token-accounting.d.ts +73 -0
- package/dist/token-accounting.d.ts.map +1 -0
- package/dist/token-accounting.js +129 -0
- package/dist/token-accounting.js.map +1 -0
- package/dist/tool-dispatch.d.ts +19 -0
- package/dist/tool-dispatch.d.ts.map +1 -0
- package/dist/tool-dispatch.js +118 -0
- package/dist/tool-dispatch.js.map +1 -0
- package/dist/tool-gating.d.ts +32 -0
- package/dist/tool-gating.d.ts.map +1 -0
- package/dist/tool-gating.js +83 -0
- package/dist/tool-gating.js.map +1 -0
- package/dist/tool.d.ts +166 -0
- package/dist/tool.d.ts.map +1 -0
- package/dist/tool.js +2 -0
- package/dist/tool.js.map +1 -0
- package/dist/transcriber.d.ts +64 -0
- package/dist/transcriber.d.ts.map +1 -0
- package/dist/transcriber.js +21 -0
- package/dist/transcriber.js.map +1 -0
- package/dist/tunnel.d.ts +25 -0
- package/dist/tunnel.d.ts.map +1 -0
- package/dist/tunnel.js +2 -0
- package/dist/tunnel.js.map +1 -0
- package/dist/type-contracts.d.ts +2 -0
- package/dist/type-contracts.d.ts.map +1 -0
- package/dist/type-contracts.js +4 -0
- package/dist/type-contracts.js.map +1 -0
- package/dist/types.test-d.d.ts +2 -0
- package/dist/types.test-d.d.ts.map +1 -0
- package/dist/types.test-d.js +22 -0
- package/dist/types.test-d.js.map +1 -0
- package/dist/view-renderer.d.ts +97 -0
- package/dist/view-renderer.d.ts.map +1 -0
- package/dist/view-renderer.js +72 -0
- package/dist/view-renderer.js.map +1 -0
- package/package.json +38 -0
- package/src/agent.ts +38 -0
- package/src/cache-strategy.ts +39 -0
- package/src/channel.ts +158 -0
- package/src/client-session.ts +88 -0
- package/src/command.ts +86 -0
- package/src/compactor-helpers.test.ts +206 -0
- package/src/compactor-helpers.ts +163 -0
- package/src/compactor.ts +20 -0
- package/src/define.test.ts +76 -0
- package/src/define.ts +113 -0
- package/src/elision-helpers.ts +143 -0
- package/src/elision-state.ts +170 -0
- package/src/embedding-cache.ts +0 -0
- package/src/embedding.ts +22 -0
- package/src/errors.test.ts +176 -0
- package/src/errors.ts +308 -0
- package/src/events.ts +250 -0
- package/src/hooks.ts +54 -0
- package/src/ids.ts +16 -0
- package/src/index.ts +304 -0
- package/src/install-hints.ts +43 -0
- package/src/isolation.ts +148 -0
- package/src/log.ts +11 -0
- package/src/loop-helpers.test.ts +78 -0
- package/src/mode-helpers.ts +483 -0
- package/src/mode.ts +139 -0
- package/src/package-root.test.ts +40 -0
- package/src/permission.ts +37 -0
- package/src/plugin.ts +92 -0
- package/src/provider-utils.test.ts +84 -0
- package/src/provider-utils.ts +171 -0
- package/src/provider.ts +195 -0
- package/src/requirements.ts +35 -0
- package/src/schemas.test.ts +106 -0
- package/src/schemas.ts +97 -0
- package/src/session-like.ts +118 -0
- package/src/skill.ts +34 -0
- package/src/subagent.ts +46 -0
- package/src/token-accounting.test.ts +93 -0
- package/src/token-accounting.ts +202 -0
- package/src/token-efficiency.test.ts +459 -0
- package/src/tool-dispatch.ts +137 -0
- package/src/tool-gating.ts +107 -0
- package/src/tool.ts +175 -0
- package/src/transcriber.ts +71 -0
- package/src/tunnel.ts +26 -0
- package/src/type-contracts.ts +4 -0
- package/src/types.test-d.ts +29 -0
- package/src/view-renderer.test.ts +90 -0
- package/src/view-renderer.ts +156 -0
package/src/schemas.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const pluginKindSchema = z.enum([
|
|
4
|
+
'tools',
|
|
5
|
+
'provider',
|
|
6
|
+
'mode',
|
|
7
|
+
'compactor',
|
|
8
|
+
'cache-strategy',
|
|
9
|
+
'view-renderer',
|
|
10
|
+
'tunnel-provider',
|
|
11
|
+
'mcp',
|
|
12
|
+
'cli',
|
|
13
|
+
'channel',
|
|
14
|
+
'hooks',
|
|
15
|
+
'agent',
|
|
16
|
+
'command',
|
|
17
|
+
'transcriber',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
export const requirementSchema = z.object({
|
|
21
|
+
kind: z.enum([
|
|
22
|
+
'plugin',
|
|
23
|
+
'provider',
|
|
24
|
+
'tool',
|
|
25
|
+
'transcriber',
|
|
26
|
+
'mode',
|
|
27
|
+
'compactor',
|
|
28
|
+
'channel',
|
|
29
|
+
'agent',
|
|
30
|
+
'command',
|
|
31
|
+
'runtime',
|
|
32
|
+
]),
|
|
33
|
+
name: z.string().min(1),
|
|
34
|
+
state: z.enum(['registered', 'active', 'ready']).optional(),
|
|
35
|
+
version: z.string().min(1).optional(),
|
|
36
|
+
optional: z.boolean().optional(),
|
|
37
|
+
reason: z.string().min(1).optional(),
|
|
38
|
+
hint: z.string().min(1).optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Optional schedule block on a skill. When present, the scheduler
|
|
43
|
+
* plugin (if installed) automatically registers a recurring or one-shot
|
|
44
|
+
* trigger that runs the skill body as a prompt. Either `cron` or
|
|
45
|
+
* `runAt` must be set; supplying both is rejected by the scheduler.
|
|
46
|
+
*/
|
|
47
|
+
export const skillScheduleSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
cron: z.string().min(1).optional(),
|
|
50
|
+
runAt: z
|
|
51
|
+
.union([z.number().int(), z.string().min(1)])
|
|
52
|
+
.optional(),
|
|
53
|
+
timeZone: z.string().min(1).optional(),
|
|
54
|
+
channel: z.string().min(1).optional(),
|
|
55
|
+
enabled: z.boolean().optional(),
|
|
56
|
+
})
|
|
57
|
+
.refine((v) => !!v.cron || v.runAt !== undefined, {
|
|
58
|
+
message: 'schedule needs either `cron` or `runAt`',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const skillFrontmatterSchema = z.object({
|
|
62
|
+
name: z.string().min(1).max(120).regex(/^[a-z0-9][a-z0-9-]*$/, 'name must be slug-like'),
|
|
63
|
+
description: z.string().min(1).max(240),
|
|
64
|
+
triggers: z.array(z.string().min(1)).optional(),
|
|
65
|
+
'allowed-tools': z.array(z.string().min(1)).optional(),
|
|
66
|
+
version: z.string().optional(),
|
|
67
|
+
tags: z.array(z.string().min(1)).optional(),
|
|
68
|
+
/** Opt the skill into automatic recurring/one-shot execution. */
|
|
69
|
+
schedule: skillScheduleSchema.optional(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const pluginManifestSchema = z.object({
|
|
73
|
+
entry: z.string().min(1),
|
|
74
|
+
kind: z
|
|
75
|
+
.union([pluginKindSchema, z.array(pluginKindSchema)])
|
|
76
|
+
.optional(),
|
|
77
|
+
skills: z.string().optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Shape of a package's `moxxy` field in package.json.
|
|
82
|
+
*
|
|
83
|
+
* - `plugin` — the per-package plugin manifest (`entry`, `kind`, `skills`).
|
|
84
|
+
* When omitted the package is not treated as a moxxy plugin.
|
|
85
|
+
* - `requirements` — declarative prerequisites that gate plugin
|
|
86
|
+
* registration and drive load-order toposort. This is the SINGLE place
|
|
87
|
+
* requirements may be authored; per-tool/per-transcriber/per-anything
|
|
88
|
+
* runtime declarations were removed in favor of static analysis.
|
|
89
|
+
*/
|
|
90
|
+
export const moxxyPackageSchema = z.object({
|
|
91
|
+
plugin: pluginManifestSchema.optional(),
|
|
92
|
+
requirements: z.array(requirementSchema).optional(),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export type SkillFrontmatterInput = z.infer<typeof skillFrontmatterSchema>;
|
|
96
|
+
export type PluginManifestInput = z.infer<typeof pluginManifestSchema>;
|
|
97
|
+
export type MoxxyPackageInput = z.infer<typeof moxxyPackageSchema>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { MoxxyEvent, UserPromptAttachment } from './events.js';
|
|
2
|
+
import type { SessionId, TurnId } from './ids.js';
|
|
3
|
+
import type { EventLogReader } from './log.js';
|
|
4
|
+
import type { ApprovalResolver } from './mode.js';
|
|
5
|
+
import type { PermissionResolver } from './permission.js';
|
|
6
|
+
import type { ModelDescriptor } from './provider.js';
|
|
7
|
+
import type { ToolCompactPresentation } from './tool.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Options accepted by `SessionLike.runTurn`. Defined here (rather than in
|
|
11
|
+
* `@moxxy/core`) so the runner client and any consumer can reference it
|
|
12
|
+
* without importing the runtime. `@moxxy/core` re-exports it.
|
|
13
|
+
*/
|
|
14
|
+
export interface RunTurnOptions {
|
|
15
|
+
readonly model?: string;
|
|
16
|
+
readonly systemPrompt?: string;
|
|
17
|
+
readonly maxIterations?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Per-turn abort signal. Aborting it cancels this turn without tainting
|
|
20
|
+
* the session's own controller (e.g. "user hit Esc on a runaway loop").
|
|
21
|
+
*/
|
|
22
|
+
readonly signal?: AbortSignal;
|
|
23
|
+
/** Inline attachments shipped alongside the prompt (images, audio, stdin). */
|
|
24
|
+
readonly attachments?: ReadonlyArray<UserPromptAttachment>;
|
|
25
|
+
/**
|
|
26
|
+
* Pre-minted turn id. When omitted, `runTurn` mints one. The runner passes
|
|
27
|
+
* this so it can return the id to the client *before* the turn starts and
|
|
28
|
+
* associate per-turn permission prompts with the originating client.
|
|
29
|
+
*/
|
|
30
|
+
readonly turnId?: TurnId;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The read side of the event log plus the live subscription a channel needs
|
|
35
|
+
* to render in real time. A `RemoteSession` backs this with a local mirror
|
|
36
|
+
* fed by the runner's event stream; a local `Session` backs it with the real
|
|
37
|
+
* `EventLog`.
|
|
38
|
+
*/
|
|
39
|
+
export interface SessionLogReader extends EventLogReader {
|
|
40
|
+
subscribe(fn: (event: MoxxyEvent) => void | Promise<void>): () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Serializable provider metadata (models + context windows) for display. */
|
|
44
|
+
export interface ProviderInfo {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly models: ReadonlyArray<ModelDescriptor>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Serializable tool metadata for status lines / slash menus / compact rendering. */
|
|
50
|
+
export interface ToolInfo {
|
|
51
|
+
readonly name: string;
|
|
52
|
+
readonly description: string;
|
|
53
|
+
/** Compact presentation hint (plain data - crosses the wire intact). */
|
|
54
|
+
readonly compact?: ToolCompactPresentation;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Serializable skill metadata. */
|
|
58
|
+
export interface SkillInfo {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly name: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Serializable slash-command metadata for the picker / `/help`. */
|
|
64
|
+
export interface CommandInfo {
|
|
65
|
+
readonly name: string;
|
|
66
|
+
readonly description: string;
|
|
67
|
+
readonly aliases?: ReadonlyArray<string>;
|
|
68
|
+
readonly channels?: ReadonlyArray<string>;
|
|
69
|
+
readonly pendingNotice?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A wire-friendly snapshot of a session's registries - everything a channel
|
|
74
|
+
* needs to *render* (status line, pickers, slash suggestions) without
|
|
75
|
+
* reaching into live registry objects (`LLMProvider`, `ModeDef`, `ToolDef`)
|
|
76
|
+
* whose methods can't cross a transport. A local `Session` builds it from its
|
|
77
|
+
* registries; a `RemoteSession` fetches it from the runner and refreshes it
|
|
78
|
+
* when the runner reports `info.changed`.
|
|
79
|
+
*/
|
|
80
|
+
export interface SessionInfo {
|
|
81
|
+
readonly sessionId: SessionId;
|
|
82
|
+
readonly cwd: string;
|
|
83
|
+
readonly activeProvider: string | null;
|
|
84
|
+
readonly providers: ReadonlyArray<ProviderInfo>;
|
|
85
|
+
readonly activeMode: string | null;
|
|
86
|
+
readonly modes: ReadonlyArray<string>;
|
|
87
|
+
readonly tools: ReadonlyArray<ToolInfo>;
|
|
88
|
+
readonly skills: ReadonlyArray<SkillInfo>;
|
|
89
|
+
readonly commands: ReadonlyArray<CommandInfo>;
|
|
90
|
+
/** Provider names the runner has activated (credentials resolved). */
|
|
91
|
+
readonly readyProviders: ReadonlyArray<string>;
|
|
92
|
+
readonly hasTranscriber: boolean;
|
|
93
|
+
/** Name of the active transcriber, or null. Lets a thin client proxy STT. */
|
|
94
|
+
readonly activeTranscriber: string | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The session surface a `Channel` depends on, decoupled from whether the
|
|
99
|
+
* session runs in-process (`@moxxy/core`'s `Session`) or is a thin-client
|
|
100
|
+
* proxy (`RemoteSession` from `@moxxy/runner`). The same channel code drives
|
|
101
|
+
* both - the runner/thin-client split hinges on this interface.
|
|
102
|
+
*
|
|
103
|
+
* Behavioral methods (`runTurn`, resolvers, `close`) and the live event log
|
|
104
|
+
* are the contract; richer registry *behavior* (executing a tool, streaming a
|
|
105
|
+
* provider) stays server-side and is never exposed here. For display, use the
|
|
106
|
+
* serializable `getInfo()` snapshot instead of live registry objects.
|
|
107
|
+
*/
|
|
108
|
+
export interface SessionLike {
|
|
109
|
+
readonly id: SessionId;
|
|
110
|
+
readonly cwd: string;
|
|
111
|
+
readonly log: SessionLogReader;
|
|
112
|
+
runTurn(prompt: string, opts?: RunTurnOptions): AsyncIterable<MoxxyEvent>;
|
|
113
|
+
setPermissionResolver(resolver: PermissionResolver): void;
|
|
114
|
+
setApprovalResolver(resolver: ApprovalResolver | null): void;
|
|
115
|
+
/** Wire-friendly registry snapshot for rendering. */
|
|
116
|
+
getInfo(): SessionInfo;
|
|
117
|
+
close(reason?: string): Promise<void>;
|
|
118
|
+
}
|
package/src/skill.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { SkillId } from './ids.js';
|
|
2
|
+
|
|
3
|
+
export interface SkillSchedule {
|
|
4
|
+
readonly cron?: string;
|
|
5
|
+
readonly runAt?: number | string;
|
|
6
|
+
readonly timeZone?: string;
|
|
7
|
+
readonly channel?: string;
|
|
8
|
+
readonly enabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SkillFrontmatter {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description: string;
|
|
14
|
+
readonly triggers?: ReadonlyArray<string>;
|
|
15
|
+
readonly 'allowed-tools'?: ReadonlyArray<string>;
|
|
16
|
+
readonly version?: string;
|
|
17
|
+
readonly tags?: ReadonlyArray<string>;
|
|
18
|
+
readonly schedule?: SkillSchedule;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SkillScope = 'project' | 'user' | 'plugin' | 'builtin';
|
|
22
|
+
|
|
23
|
+
export interface Skill {
|
|
24
|
+
readonly id: SkillId;
|
|
25
|
+
readonly path: string;
|
|
26
|
+
readonly scope: SkillScope;
|
|
27
|
+
readonly frontmatter: SkillFrontmatter;
|
|
28
|
+
readonly body: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SkillDef {
|
|
32
|
+
readonly frontmatter: SkillFrontmatter;
|
|
33
|
+
readonly body: string;
|
|
34
|
+
}
|
package/src/subagent.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent primitives — let a running loop strategy or tool spawn one or
|
|
3
|
+
* more child agents that share the parent's tools / skills / providers /
|
|
4
|
+
* permissions but have an isolated event log and (typically) a focused
|
|
5
|
+
* task prompt. Children stream their work back to the parent log as
|
|
6
|
+
* `plugin_event` records with `subagent_*` subtypes, so the TUI and other
|
|
7
|
+
* subscribers can render progress live without waiting for the final
|
|
8
|
+
* message.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SessionId } from './ids.js';
|
|
12
|
+
import type { StopReason } from './provider-utils.js';
|
|
13
|
+
|
|
14
|
+
export interface SubagentSpec {
|
|
15
|
+
/** The user message the child sees as its prompt. */
|
|
16
|
+
readonly prompt: string;
|
|
17
|
+
/** Optional system prompt override for the child. */
|
|
18
|
+
readonly systemPrompt?: string;
|
|
19
|
+
/** Override model id; defaults to the parent's active model. */
|
|
20
|
+
readonly model?: string;
|
|
21
|
+
/** Mode name to run inside the child (default: `'tool-use'`). */
|
|
22
|
+
readonly mode?: string;
|
|
23
|
+
/** Per-child iteration cap (default 50). */
|
|
24
|
+
readonly maxIterations?: number;
|
|
25
|
+
/** Restrict the child to these tools by name. Omit for full inheritance. */
|
|
26
|
+
readonly allowedTools?: ReadonlyArray<string>;
|
|
27
|
+
/** Human-readable label surfaced in `subagent_*` event payloads. */
|
|
28
|
+
readonly label?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface SubagentResult {
|
|
32
|
+
readonly label: string;
|
|
33
|
+
readonly childSessionId: SessionId;
|
|
34
|
+
/** The child's final assistant text (or last non-empty text if it ended on a tool call). */
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly stopReason: StopReason;
|
|
37
|
+
/** Populated when the child loop errored fatally or threw. */
|
|
38
|
+
readonly error?: { readonly message: string };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface SubagentSpawner {
|
|
42
|
+
/** Run a single child to completion. */
|
|
43
|
+
spawn(spec: SubagentSpec): Promise<SubagentResult>;
|
|
44
|
+
/** Run N children in parallel; resolves with results in input order. */
|
|
45
|
+
spawnAll(specs: ReadonlyArray<SubagentSpec>): Promise<ReadonlyArray<SubagentResult>>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
addModelTotals,
|
|
4
|
+
asEventId,
|
|
5
|
+
asSessionId,
|
|
6
|
+
asTurnId,
|
|
7
|
+
summarizeTokensByModel,
|
|
8
|
+
type ModelUsageTotals,
|
|
9
|
+
type MoxxyEvent,
|
|
10
|
+
} from './index.js';
|
|
11
|
+
|
|
12
|
+
const sid = asSessionId('s1');
|
|
13
|
+
const t1 = asTurnId('t1');
|
|
14
|
+
|
|
15
|
+
function resp(seq: number, partial: Partial<Extract<MoxxyEvent, { type: 'provider_response' }>>): MoxxyEvent {
|
|
16
|
+
return {
|
|
17
|
+
id: asEventId(`e${seq}`),
|
|
18
|
+
seq,
|
|
19
|
+
ts: seq,
|
|
20
|
+
sessionId: sid,
|
|
21
|
+
turnId: t1,
|
|
22
|
+
source: 'system',
|
|
23
|
+
type: 'provider_response',
|
|
24
|
+
provider: 'anthropic',
|
|
25
|
+
model: 'claude-opus-4-7',
|
|
26
|
+
...partial,
|
|
27
|
+
} as MoxxyEvent;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('summarizeTokensByModel', () => {
|
|
31
|
+
it('groups usage by provider/model and sums fields', () => {
|
|
32
|
+
const byModel = summarizeTokensByModel([
|
|
33
|
+
resp(0, { provider: 'anthropic', model: 'opus', inputTokens: 100, outputTokens: 10 }),
|
|
34
|
+
resp(1, {
|
|
35
|
+
provider: 'anthropic',
|
|
36
|
+
model: 'opus',
|
|
37
|
+
inputTokens: 50,
|
|
38
|
+
outputTokens: 5,
|
|
39
|
+
cacheReadTokens: 200,
|
|
40
|
+
cacheCreationTokens: 30,
|
|
41
|
+
}),
|
|
42
|
+
resp(2, { provider: 'openai', model: 'gpt-5', inputTokens: 80, outputTokens: 8 }),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
expect(Object.keys(byModel).sort()).toEqual(['anthropic/opus', 'openai/gpt-5']);
|
|
46
|
+
expect(byModel['anthropic/opus']).toEqual<ModelUsageTotals>({
|
|
47
|
+
calls: 2,
|
|
48
|
+
inputTokens: 150,
|
|
49
|
+
outputTokens: 15,
|
|
50
|
+
cacheReadTokens: 200,
|
|
51
|
+
cacheCreationTokens: 30,
|
|
52
|
+
});
|
|
53
|
+
expect(byModel['openai/gpt-5']!.calls).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('keeps the same model id under different providers separate', () => {
|
|
57
|
+
const byModel = summarizeTokensByModel([
|
|
58
|
+
resp(0, { provider: 'a', model: 'm', inputTokens: 1 }),
|
|
59
|
+
resp(1, { provider: 'b', model: 'm', inputTokens: 2 }),
|
|
60
|
+
]);
|
|
61
|
+
expect(byModel['a/m']!.inputTokens).toBe(1);
|
|
62
|
+
expect(byModel['b/m']!.inputTokens).toBe(2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('skips provider_response events that reported no usage', () => {
|
|
66
|
+
const byModel = summarizeTokensByModel([
|
|
67
|
+
resp(0, { provider: 'a', model: 'm' }), // no token fields
|
|
68
|
+
]);
|
|
69
|
+
expect(byModel).toEqual({});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('ignores non provider_response events', () => {
|
|
73
|
+
const byModel = summarizeTokensByModel([
|
|
74
|
+
{ id: asEventId('u'), seq: 0, ts: 0, sessionId: sid, turnId: t1, source: 'user', type: 'user_prompt', text: 'hi' } as MoxxyEvent,
|
|
75
|
+
resp(1, { inputTokens: 5 }),
|
|
76
|
+
]);
|
|
77
|
+
expect(byModel['anthropic/claude-opus-4-7']!.inputTokens).toBe(5);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('addModelTotals', () => {
|
|
82
|
+
it('adds field-wise', () => {
|
|
83
|
+
const a: ModelUsageTotals = { calls: 1, inputTokens: 10, outputTokens: 2, cacheReadTokens: 3, cacheCreationTokens: 4 };
|
|
84
|
+
const b: ModelUsageTotals = { calls: 2, inputTokens: 20, outputTokens: 5, cacheReadTokens: 1, cacheCreationTokens: 0 };
|
|
85
|
+
expect(addModelTotals(a, b)).toEqual<ModelUsageTotals>({
|
|
86
|
+
calls: 3,
|
|
87
|
+
inputTokens: 30,
|
|
88
|
+
outputTokens: 7,
|
|
89
|
+
cacheReadTokens: 4,
|
|
90
|
+
cacheCreationTokens: 4,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { MoxxyEvent, ProviderResponseEvent } from './events.js';
|
|
2
|
+
import type { EventLogReader } from './log.js';
|
|
3
|
+
import type { TokenUsage } from './provider.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Token accounting derived from the event log. The single source of truth is
|
|
7
|
+
* the stream of `provider_response` events (each carries the provider-reported
|
|
8
|
+
* usage for one call); cumulative session totals are just a fold over them, so
|
|
9
|
+
* there is no separate mutable counter to keep in sync.
|
|
10
|
+
*
|
|
11
|
+
* Anthropic usage semantics (which this models): `inputTokens` is the
|
|
12
|
+
* NON-cached portion of the prompt; cache reads and cache writes are reported
|
|
13
|
+
* separately and are NOT included in `inputTokens`. So the full prompt size of
|
|
14
|
+
* a call is `input + cacheRead + cacheCreation`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Relative price multipliers vs. an uncached input token (Anthropic ephemeral
|
|
18
|
+
// cache pricing). Kept here so the savings math is explicit and tweakable.
|
|
19
|
+
const CACHE_READ_MULT = 0.1;
|
|
20
|
+
const CACHE_WRITE_MULT = 1.25;
|
|
21
|
+
|
|
22
|
+
/** Partial `provider_response` fields for a given usage; `{}` when usage is absent. */
|
|
23
|
+
export function usageEventFields(usage?: TokenUsage): {
|
|
24
|
+
inputTokens?: number;
|
|
25
|
+
outputTokens?: number;
|
|
26
|
+
cacheReadTokens?: number;
|
|
27
|
+
cacheCreationTokens?: number;
|
|
28
|
+
} {
|
|
29
|
+
if (!usage) return {};
|
|
30
|
+
return {
|
|
31
|
+
inputTokens: usage.inputTokens,
|
|
32
|
+
outputTokens: usage.outputTokens,
|
|
33
|
+
...(usage.cacheReadTokens !== undefined ? { cacheReadTokens: usage.cacheReadTokens } : {}),
|
|
34
|
+
...(usage.cacheCreationTokens !== undefined
|
|
35
|
+
? { cacheCreationTokens: usage.cacheCreationTokens }
|
|
36
|
+
: {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SessionTokenSummary {
|
|
41
|
+
/** Number of provider calls that reported usage. */
|
|
42
|
+
readonly calls: number;
|
|
43
|
+
/** Sum of non-cached input tokens (billed 1.0x). */
|
|
44
|
+
readonly totalInput: number;
|
|
45
|
+
/** Sum of cache-read tokens (billed 0.1x). */
|
|
46
|
+
readonly totalCacheRead: number;
|
|
47
|
+
/** Sum of cache-creation/write tokens (billed 1.25x). */
|
|
48
|
+
readonly totalCacheCreation: number;
|
|
49
|
+
/** Sum of output tokens. */
|
|
50
|
+
readonly totalOutput: number;
|
|
51
|
+
/** Total prompt tokens fed to the model across the session (input + reads + writes). */
|
|
52
|
+
readonly totalPrompt: number;
|
|
53
|
+
/** cacheRead / totalPrompt — fraction of prompt served from cache. */
|
|
54
|
+
readonly cacheHitRate: number;
|
|
55
|
+
/** Billed-equivalent input cost (read 0.1x + write 1.25x + plain 1.0x), in token units. */
|
|
56
|
+
readonly billedInputEq: number;
|
|
57
|
+
/** What the input would cost with no caching (every prompt token at 1.0x). */
|
|
58
|
+
readonly uncachedInputEq: number;
|
|
59
|
+
/** 1 - billedInputEq/uncachedInputEq — fraction of input cost saved by caching. */
|
|
60
|
+
readonly savedRatio: number;
|
|
61
|
+
/**
|
|
62
|
+
* False only when caching is clearly broken: across enough calls the provider
|
|
63
|
+
* is *writing* cache (cacheCreation > 0) but almost never *reading* it back —
|
|
64
|
+
* the signature of an unstable prefix silently paying the 1.25x write tax.
|
|
65
|
+
* True when caching works OR is simply off (no writes) — so this never
|
|
66
|
+
* false-alarms a deliberately disabled cache.
|
|
67
|
+
*/
|
|
68
|
+
readonly cacheEffective: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Fold `provider_response` events into cumulative per-session token totals. */
|
|
72
|
+
export function summarizeSessionTokens(log: EventLogReader): SessionTokenSummary {
|
|
73
|
+
return foldResponses(log.ofType('provider_response'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Same as {@link summarizeSessionTokens} but over a raw event array — for
|
|
78
|
+
* channels (e.g. the TUI) that hold the materialized event stream rather than
|
|
79
|
+
* an `EventLogReader`.
|
|
80
|
+
*/
|
|
81
|
+
export function summarizeSessionTokensFromEvents(
|
|
82
|
+
events: ReadonlyArray<MoxxyEvent>,
|
|
83
|
+
): SessionTokenSummary {
|
|
84
|
+
return foldResponses(
|
|
85
|
+
events.filter((e): e is ProviderResponseEvent => e.type === 'provider_response'),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Raw per-model token totals — the shape persisted to the cross-session usage
|
|
91
|
+
* aggregate (`~/.moxxy/usage.json`). Unlike {@link SessionTokenSummary} this
|
|
92
|
+
* carries no derived/cost fields: it's a plain additive counter so totals from
|
|
93
|
+
* many sessions can be summed without re-deriving ratios.
|
|
94
|
+
*/
|
|
95
|
+
export interface ModelUsageTotals {
|
|
96
|
+
/** Provider calls that reported usage for this model. */
|
|
97
|
+
readonly calls: number;
|
|
98
|
+
readonly inputTokens: number;
|
|
99
|
+
readonly outputTokens: number;
|
|
100
|
+
readonly cacheReadTokens: number;
|
|
101
|
+
readonly cacheCreationTokens: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ZERO_TOTALS: ModelUsageTotals = {
|
|
105
|
+
calls: 0,
|
|
106
|
+
inputTokens: 0,
|
|
107
|
+
outputTokens: 0,
|
|
108
|
+
cacheReadTokens: 0,
|
|
109
|
+
cacheCreationTokens: 0,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Add two {@link ModelUsageTotals} field-wise (for merging session deltas into the aggregate). */
|
|
113
|
+
export function addModelTotals(a: ModelUsageTotals, b: ModelUsageTotals): ModelUsageTotals {
|
|
114
|
+
return {
|
|
115
|
+
calls: a.calls + b.calls,
|
|
116
|
+
inputTokens: a.inputTokens + b.inputTokens,
|
|
117
|
+
outputTokens: a.outputTokens + b.outputTokens,
|
|
118
|
+
cacheReadTokens: a.cacheReadTokens + b.cacheReadTokens,
|
|
119
|
+
cacheCreationTokens: a.cacheCreationTokens + b.cacheCreationTokens,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A `provider_response` reported usage iff any token field is present. */
|
|
124
|
+
function hasUsage(e: ProviderResponseEvent): boolean {
|
|
125
|
+
return (
|
|
126
|
+
e.inputTokens !== undefined ||
|
|
127
|
+
e.outputTokens !== undefined ||
|
|
128
|
+
e.cacheReadTokens !== undefined ||
|
|
129
|
+
e.cacheCreationTokens !== undefined
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Fold `provider_response` events into per-model token totals, keyed by
|
|
135
|
+
* `"<provider>/<model>"` (the same model id can be served by more than one
|
|
136
|
+
* provider, so both pin the key). Used by the usage-stats plugin to compute a
|
|
137
|
+
* session's contribution and by the `/usage` panel to render the lifetime
|
|
138
|
+
* breakdown.
|
|
139
|
+
*/
|
|
140
|
+
export function summarizeTokensByModel(
|
|
141
|
+
events: ReadonlyArray<MoxxyEvent>,
|
|
142
|
+
): Record<string, ModelUsageTotals> {
|
|
143
|
+
const byModel: Record<string, ModelUsageTotals> = {};
|
|
144
|
+
for (const e of events) {
|
|
145
|
+
if (e.type !== 'provider_response') continue;
|
|
146
|
+
if (!hasUsage(e)) continue;
|
|
147
|
+
const key = `${e.provider}/${e.model}`;
|
|
148
|
+
byModel[key] = addModelTotals(byModel[key] ?? ZERO_TOTALS, {
|
|
149
|
+
calls: 1,
|
|
150
|
+
inputTokens: e.inputTokens ?? 0,
|
|
151
|
+
outputTokens: e.outputTokens ?? 0,
|
|
152
|
+
cacheReadTokens: e.cacheReadTokens ?? 0,
|
|
153
|
+
cacheCreationTokens: e.cacheCreationTokens ?? 0,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return byModel;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function foldResponses(responses: ReadonlyArray<ProviderResponseEvent>): SessionTokenSummary {
|
|
160
|
+
let calls = 0;
|
|
161
|
+
let totalInput = 0;
|
|
162
|
+
let totalCacheRead = 0;
|
|
163
|
+
let totalCacheCreation = 0;
|
|
164
|
+
let totalOutput = 0;
|
|
165
|
+
|
|
166
|
+
for (const e of responses) {
|
|
167
|
+
// Count a call only when it reported any token data.
|
|
168
|
+
if (
|
|
169
|
+
e.inputTokens === undefined &&
|
|
170
|
+
e.outputTokens === undefined &&
|
|
171
|
+
e.cacheReadTokens === undefined &&
|
|
172
|
+
e.cacheCreationTokens === undefined
|
|
173
|
+
) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
calls += 1;
|
|
177
|
+
totalInput += e.inputTokens ?? 0;
|
|
178
|
+
totalCacheRead += e.cacheReadTokens ?? 0;
|
|
179
|
+
totalCacheCreation += e.cacheCreationTokens ?? 0;
|
|
180
|
+
totalOutput += e.outputTokens ?? 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const totalPrompt = totalInput + totalCacheRead + totalCacheCreation;
|
|
184
|
+
const billedInputEq =
|
|
185
|
+
totalInput + totalCacheRead * CACHE_READ_MULT + totalCacheCreation * CACHE_WRITE_MULT;
|
|
186
|
+
const uncachedInputEq = totalPrompt;
|
|
187
|
+
const cacheHitRate = totalPrompt > 0 ? totalCacheRead / totalPrompt : 0;
|
|
188
|
+
return {
|
|
189
|
+
calls,
|
|
190
|
+
totalInput,
|
|
191
|
+
totalCacheRead,
|
|
192
|
+
totalCacheCreation,
|
|
193
|
+
totalOutput,
|
|
194
|
+
totalPrompt,
|
|
195
|
+
cacheHitRate,
|
|
196
|
+
billedInputEq,
|
|
197
|
+
uncachedInputEq,
|
|
198
|
+
savedRatio: uncachedInputEq > 0 ? 1 - billedInputEq / uncachedInputEq : 0,
|
|
199
|
+
// Broken = writing cache but not reading it back, over enough calls.
|
|
200
|
+
cacheEffective: !(calls >= 5 && totalCacheCreation > 0 && cacheHitRate < 0.05),
|
|
201
|
+
};
|
|
202
|
+
}
|