@moxxy/sdk 0.1.2 → 0.1.3

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.
Files changed (96) hide show
  1. package/README.md +73 -73
  2. package/dist/compactor-helpers.d.ts +4 -1
  3. package/dist/compactor-helpers.d.ts.map +1 -1
  4. package/dist/compactor-helpers.js +50 -21
  5. package/dist/compactor-helpers.js.map +1 -1
  6. package/dist/define.d.ts +4 -0
  7. package/dist/define.d.ts.map +1 -1
  8. package/dist/define.js +6 -0
  9. package/dist/define.js.map +1 -1
  10. package/dist/embedding.d.ts +17 -0
  11. package/dist/embedding.d.ts.map +1 -1
  12. package/dist/errors.d.ts +1 -1
  13. package/dist/errors.d.ts.map +1 -1
  14. package/dist/errors.js.map +1 -1
  15. package/dist/fs-utils.d.ts +27 -0
  16. package/dist/fs-utils.d.ts.map +1 -0
  17. package/dist/fs-utils.js +44 -0
  18. package/dist/fs-utils.js.map +1 -0
  19. package/dist/http-utils.d.ts +16 -0
  20. package/dist/http-utils.d.ts.map +1 -0
  21. package/dist/http-utils.js +40 -0
  22. package/dist/http-utils.js.map +1 -0
  23. package/dist/index.d.ts +12 -6
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -4
  26. package/dist/index.js.map +1 -1
  27. package/dist/mode-helpers.d.ts +58 -0
  28. package/dist/mode-helpers.d.ts.map +1 -1
  29. package/dist/mode-helpers.js +119 -0
  30. package/dist/mode-helpers.js.map +1 -1
  31. package/dist/mode.d.ts +8 -0
  32. package/dist/mode.d.ts.map +1 -1
  33. package/dist/mutex.d.ts +15 -0
  34. package/dist/mutex.d.ts.map +1 -0
  35. package/dist/mutex.js +15 -0
  36. package/dist/mutex.js.map +1 -0
  37. package/dist/plugin.d.ts +27 -1
  38. package/dist/plugin.d.ts.map +1 -1
  39. package/dist/provider-utils.d.ts +6 -0
  40. package/dist/provider-utils.d.ts.map +1 -1
  41. package/dist/provider-utils.js +8 -0
  42. package/dist/provider-utils.js.map +1 -1
  43. package/dist/resolvers.d.ts +52 -0
  44. package/dist/resolvers.d.ts.map +1 -0
  45. package/dist/resolvers.js +123 -0
  46. package/dist/resolvers.js.map +1 -0
  47. package/dist/schemas.d.ts +24 -24
  48. package/dist/session-like.d.ts +85 -1
  49. package/dist/session-like.d.ts.map +1 -1
  50. package/dist/voice.d.ts +36 -0
  51. package/dist/voice.d.ts.map +1 -0
  52. package/dist/voice.js +82 -0
  53. package/dist/voice.js.map +1 -0
  54. package/dist/workflow.d.ts +144 -0
  55. package/dist/workflow.d.ts.map +1 -0
  56. package/dist/workflow.js +14 -0
  57. package/dist/workflow.js.map +1 -0
  58. package/package.json +14 -14
  59. package/src/compactor-helpers.test.ts +41 -0
  60. package/src/compactor-helpers.ts +53 -19
  61. package/src/define.ts +10 -0
  62. package/src/embedding.ts +18 -0
  63. package/src/errors.ts +3 -0
  64. package/src/fs-utils.test.ts +61 -0
  65. package/src/fs-utils.ts +55 -0
  66. package/src/http-utils.test.ts +36 -0
  67. package/src/http-utils.ts +40 -0
  68. package/src/index.ts +58 -3
  69. package/src/mode-helpers.ts +169 -0
  70. package/src/mode.ts +8 -0
  71. package/src/mutex.test.ts +36 -0
  72. package/src/mutex.ts +31 -0
  73. package/src/plugin.ts +27 -1
  74. package/src/provider-utils.ts +9 -0
  75. package/src/resolvers.test.ts +45 -0
  76. package/src/resolvers.ts +164 -0
  77. package/src/session-like.ts +87 -1
  78. package/src/stuck-loop.test.ts +42 -0
  79. package/src/voice.ts +103 -0
  80. package/src/workflow.ts +165 -0
  81. package/dist/command-sync.d.ts +0 -23
  82. package/dist/command-sync.d.ts.map +0 -1
  83. package/dist/command-sync.js +0 -36
  84. package/dist/command-sync.js.map +0 -1
  85. package/dist/loop-helpers.d.ts +0 -68
  86. package/dist/loop-helpers.d.ts.map +0 -1
  87. package/dist/loop-helpers.js +0 -283
  88. package/dist/loop-helpers.js.map +0 -1
  89. package/dist/loop.d.ts +0 -106
  90. package/dist/loop.d.ts.map +0 -1
  91. package/dist/loop.js +0 -2
  92. package/dist/loop.js.map +0 -1
  93. package/dist/plugin-kind.d.ts +0 -10
  94. package/dist/plugin-kind.d.ts.map +0 -1
  95. package/dist/plugin-kind.js +0 -31
  96. package/dist/plugin-kind.js.map +0 -1
package/dist/voice.js ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Voice / transcription helpers shared across surfaces.
3
+ *
4
+ * The TUI's voice-input infrastructure used to inline the same logic
5
+ * with a `Codex`-specific name baked in. Pulled out here as
6
+ * *agnostic* helpers that take a transcriber name as input, so the
7
+ * desktop, TUI, and any future channel can mirror the same flow:
8
+ *
9
+ * - Is the session ready to transcribe? Check via the requirements
10
+ * API for a named transcriber. (`checkTranscriberReady`)
11
+ * - Activate any registered transcriber lazily. Returns the active
12
+ * transcriber instance. (`resolveTranscriber`)
13
+ * - "Just give me whatever works" — try a list of candidates in
14
+ * order, or fall back to the first registered one. (`pickFirstAvailableTranscriber`)
15
+ */
16
+ /** Probe whether a *named* transcriber is ready: registered, with any
17
+ * declared upstream requirements satisfied. The optional `requires`
18
+ * list lets channels gate on additional provider / auth runtimes
19
+ * (the Codex transcriber e.g. depends on the `openai-codex` provider
20
+ * + its OAuth runtime). */
21
+ export function checkTranscriberReady(session, transcriberName, requires = []) {
22
+ const baseline = [
23
+ { kind: 'transcriber', name: transcriberName },
24
+ ...requires,
25
+ ];
26
+ const check = session.requirements.check(baseline);
27
+ const activeName = session.transcribers.getActiveName();
28
+ if (!activeName || activeName === transcriberName)
29
+ return check;
30
+ const conflict = {
31
+ requirement: {
32
+ kind: 'transcriber',
33
+ name: transcriberName,
34
+ state: 'active',
35
+ hint: `Switch active transcriber to ${transcriberName}.`,
36
+ },
37
+ code: 'inactive',
38
+ message: `Required active transcriber ${transcriberName}; active is ${activeName}`,
39
+ hint: `Switch active transcriber to ${transcriberName}.`,
40
+ };
41
+ return { ready: false, issues: [conflict, ...check.issues] };
42
+ }
43
+ /** Activate a transcriber by name, lazily. Returns the active instance
44
+ * ready to `.transcribe(...)`. Throws if no such transcriber is
45
+ * registered, or a *different* one is already active. */
46
+ export function resolveTranscriber(session, transcriberName) {
47
+ const activeName = session.transcribers.getActiveName();
48
+ if (activeName && activeName !== transcriberName) {
49
+ throw new Error(`Another transcriber is already active: ${activeName}.`);
50
+ }
51
+ if (activeName === transcriberName)
52
+ return session.transcribers.getActive();
53
+ if (session.transcribers.has(transcriberName)) {
54
+ return session.transcribers.setActive(transcriberName);
55
+ }
56
+ throw new Error(`No transcriber registered as ${transcriberName}. Configure one via your moxxy plugins.`);
57
+ }
58
+ /** "Just pick a transcriber that works."
59
+ *
60
+ * Tries each name in `candidates` in order — first one that can be
61
+ * activated wins. Returns null if none can be activated, so callers
62
+ * can degrade gracefully (hide their mic button, show a "no voice
63
+ * configured" tip, …) instead of throwing. */
64
+ export function pickFirstAvailableTranscriber(session, candidates) {
65
+ // If something's already active, just hand that back — never fight
66
+ // a user-chosen activation.
67
+ const existing = session.transcribers.tryGetActive();
68
+ if (existing)
69
+ return existing;
70
+ for (const name of candidates) {
71
+ try {
72
+ return resolveTranscriber(session, name);
73
+ }
74
+ catch {
75
+ // Wrong-active errors don't apply here (we just returned early),
76
+ // so any throw is "this candidate isn't registered" — keep
77
+ // trying.
78
+ }
79
+ }
80
+ return null;
81
+ }
82
+ //# sourceMappingURL=voice.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voice.js","sourceRoot":"","sources":["../src/voice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH;;;;4BAI4B;AAC5B,MAAM,UAAU,qBAAqB,CACnC,OAAsB,EACtB,eAAuB,EACvB,WAA4C,EAAE;IAE9C,MAAM,QAAQ,GAAoC;QAChD,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,eAAe,EAAE;QAC9C,GAAG,QAAQ;KACZ,CAAC;IACF,MAAM,KAAK,GAAG,OAAO,CAAC,YAAY,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC;IACxD,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,eAAe;QAAE,OAAO,KAAK,CAAC;IAEhE,MAAM,QAAQ,GAAqB;QACjC,WAAW,EAAE;YACX,IAAI,EAAE,aAAa;YACnB,IAAI,EAAE,eAAe;YACrB,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,gCAAgC,eAAe,GAAG;SACzD;QACD,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,+BAA+B,eAAe,eAAe,UAAU,EAAE;QAClF,IAAI,EAAE,gCAAgC,eAAe,GAAG;KACzD,CAAC;IACF,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;AAC/D,CAAC;AAED;;0DAE0D;AAC1D,MAAM,UAAU,kBAAkB,CAChC,OAAsB,EACtB,eAAuB;IAEvB,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,aAAa,EAAE,CAAC;IACxD,IAAI,UAAU,IAAI,UAAU,KAAK,eAAe,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CACb,0CAA0C,UAAU,GAAG,CACxD,CAAC;IACJ,CAAC;IACD,IAAI,UAAU,KAAK,eAAe;QAAE,OAAO,OAAO,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC;IAC5E,IAAI,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,CAAC,EAAE,CAAC;QAC9C,OAAO,OAAO,CAAC,YAAY,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,IAAI,KAAK,CACb,gCAAgC,eAAe,yCAAyC,CACzF,CAAC;AACJ,CAAC;AAED;;;;;+CAK+C;AAC/C,MAAM,UAAU,6BAA6B,CAC3C,OAAsB,EACtB,UAAiC;IAEjC,mEAAmE;IACnE,4BAA4B;IAC5B,MAAM,QAAQ,GAAG,OAAO,CAAC,YAAY,CAAC,YAAY,EAAE,CAAC;IACrD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,kBAAkB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,iEAAiE;YACjE,2DAA2D;YAC3D,UAAU;QACZ,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Workflows — saved, parameterized, schedulable/event-triggered DAGs whose
3
+ * steps run a skill, a free-form prompt, a tool, or a nested workflow, piping
4
+ * each step's output into the next. A workflow is an *artifact* (authored like
5
+ * a skill, discovered from disk); the strategy that *executes* it is a
6
+ * swappable block — {@link WorkflowExecutorDef} — registered into the
7
+ * `WorkflowExecutorRegistry` and selected by name, mirroring modes/compactors.
8
+ *
9
+ * These are the shared structural types. The zod schema that validates
10
+ * on-disk YAML lives in `@moxxy/plugin-workflows`; its parsed output is
11
+ * assignable to {@link Workflow}.
12
+ */
13
+ import type { Skill } from './skill.js';
14
+ import type { SubagentSpawner } from './subagent.js';
15
+ /** What fires a workflow. Omit `on` entirely for an on-demand-only workflow. */
16
+ export interface WorkflowTrigger {
17
+ /** Cron / one-shot time trigger, dispatched by `@moxxy/plugin-scheduler`. */
18
+ readonly schedule?: {
19
+ readonly cron?: string;
20
+ readonly runAt?: number | string;
21
+ readonly timeZone?: string;
22
+ };
23
+ /** Run when the named workflow(s) complete successfully (EventLog-driven). */
24
+ readonly afterWorkflow?: string | ReadonlyArray<string>;
25
+ /** Run when files matching the glob(s) under cwd change (fs.watch-driven). */
26
+ readonly fileChanged?: string | ReadonlyArray<string>;
27
+ /** Named webhook provider whose delivery fires this workflow. */
28
+ readonly webhook?: string;
29
+ }
30
+ /** How a failed step is handled: abort the workflow, skip past it, or retry. */
31
+ export type WorkflowStepErrorMode = 'fail' | 'continue' | 'retry';
32
+ /**
33
+ * One node in the DAG. Exactly one *action* key is set
34
+ * (`skill` | `prompt` | `tool` | `workflow`). `input` is the templated prompt
35
+ * for skill/prompt actions; `args` are the templated arguments for
36
+ * tool/workflow actions. `needs` are the upstream step ids this step depends on.
37
+ */
38
+ export interface WorkflowStep {
39
+ readonly id: string;
40
+ readonly skill?: string;
41
+ readonly prompt?: string;
42
+ readonly tool?: string;
43
+ readonly workflow?: string;
44
+ readonly input?: string;
45
+ readonly args?: Record<string, unknown>;
46
+ readonly needs: ReadonlyArray<string>;
47
+ /** Condition DSL; when it evaluates false the step is skipped. */
48
+ readonly when?: string;
49
+ readonly onError: WorkflowStepErrorMode;
50
+ readonly retries: number;
51
+ readonly label?: string;
52
+ }
53
+ export interface WorkflowInputSpec {
54
+ readonly default?: unknown;
55
+ readonly description?: string;
56
+ }
57
+ export interface WorkflowDelivery {
58
+ /** Soft hint for delivery target — e.g. "telegram", "inbox". */
59
+ readonly channel?: string;
60
+ /** Also drop the final output into `~/.moxxy/inbox/`. Default true. */
61
+ readonly inbox: boolean;
62
+ }
63
+ export interface Workflow {
64
+ readonly name: string;
65
+ readonly description: string;
66
+ readonly version: number;
67
+ /**
68
+ * When false the workflow is inert: triggers (schedule/event) never fire it
69
+ * and it is excluded from auto-runs, but it stays listable and can still be
70
+ * run explicitly. Toggled from the `/workflows` command.
71
+ */
72
+ readonly enabled: boolean;
73
+ readonly inputs: Record<string, WorkflowInputSpec>;
74
+ readonly on?: WorkflowTrigger;
75
+ readonly delivery?: WorkflowDelivery;
76
+ /** Max steps to run concurrently in one ready-set round. */
77
+ readonly concurrency: number;
78
+ readonly steps: ReadonlyArray<WorkflowStep>;
79
+ }
80
+ /** Minimal tool surface a step needs — structurally a subset of `ToolRegistry`. */
81
+ export interface WorkflowToolRunner {
82
+ get(name: string): unknown | undefined;
83
+ execute(name: string, input: unknown, signal: AbortSignal): Promise<unknown>;
84
+ }
85
+ /** Look up sibling artifacts by name during a run. */
86
+ export interface WorkflowLookup {
87
+ skill(name: string): Skill | undefined;
88
+ workflow(name: string): Workflow | undefined;
89
+ }
90
+ /** Lifecycle subtypes an executor emits as `plugin_event`s (mirrors plan_*). */
91
+ export type WorkflowEventSubtype = 'workflow_started' | 'workflow_step_started' | 'workflow_step_completed' | 'workflow_step_skipped' | 'workflow_step_failed' | 'workflow_completed' | 'workflow_failed';
92
+ /**
93
+ * Everything an executor needs to run a workflow, supplied by the caller.
94
+ * The in-turn `workflow_run` tool wires `spawner` from `ctx.subagents`; the
95
+ * autonomous runner builds one from an isolated session. Kept free of core
96
+ * imports so the executor stays in a plugin.
97
+ */
98
+ export interface WorkflowRunDeps {
99
+ readonly spawner: SubagentSpawner;
100
+ readonly tools: WorkflowToolRunner;
101
+ readonly lookup: WorkflowLookup;
102
+ readonly signal: AbortSignal;
103
+ /** Resolved input values (defaults already applied) for this run. */
104
+ readonly inputs?: Record<string, unknown>;
105
+ /** Free-form description of what fired the run (for `{{ trigger }}`). */
106
+ readonly trigger?: string;
107
+ /** Wall-clock source. Injected so tests are deterministic. */
108
+ readonly now?: () => number;
109
+ /** Emit a workflow lifecycle event. No-op when omitted. */
110
+ readonly emit?: (subtype: WorkflowEventSubtype, payload: unknown) => void | Promise<void>;
111
+ readonly logger?: {
112
+ warn?(msg: string, meta?: Record<string, unknown>): void;
113
+ info?(msg: string, meta?: Record<string, unknown>): void;
114
+ };
115
+ /** Nested-workflow recursion depth; executors guard against runaway nesting. */
116
+ readonly depth?: number;
117
+ }
118
+ export type WorkflowStepStatus = 'completed' | 'skipped' | 'failed';
119
+ export interface WorkflowStepResult {
120
+ readonly id: string;
121
+ readonly status: WorkflowStepStatus;
122
+ readonly output: string;
123
+ readonly error?: string;
124
+ readonly startedAt: number;
125
+ readonly endedAt: number;
126
+ }
127
+ export interface WorkflowRunResult {
128
+ readonly ok: boolean;
129
+ readonly steps: ReadonlyArray<WorkflowStepResult>;
130
+ /** Output of the terminal (sink) step(s) — what delivery sends. */
131
+ readonly output: string;
132
+ readonly error?: string;
133
+ }
134
+ /**
135
+ * A swappable workflow-execution strategy. v1 ships one (`dag`); a plugin can
136
+ * register alternatives (parallel-heavy, dry-run, …) and the active one is
137
+ * selected by name via `session.workflowExecutors.setActive(name)`.
138
+ */
139
+ export interface WorkflowExecutorDef {
140
+ readonly name: string;
141
+ readonly description?: string;
142
+ run(workflow: Workflow, deps: WorkflowRunDeps): Promise<WorkflowRunResult>;
143
+ }
144
+ //# sourceMappingURL=workflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow.d.ts","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Workflows — saved, parameterized, schedulable/event-triggered DAGs whose
3
+ * steps run a skill, a free-form prompt, a tool, or a nested workflow, piping
4
+ * each step's output into the next. A workflow is an *artifact* (authored like
5
+ * a skill, discovered from disk); the strategy that *executes* it is a
6
+ * swappable block — {@link WorkflowExecutorDef} — registered into the
7
+ * `WorkflowExecutorRegistry` and selected by name, mirroring modes/compactors.
8
+ *
9
+ * These are the shared structural types. The zod schema that validates
10
+ * on-disk YAML lives in `@moxxy/plugin-workflows`; its parsed output is
11
+ * assignable to {@link Workflow}.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=workflow.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow.js","sourceRoot":"","sources":["../src/workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxxy/sdk",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Typed public surface for the moxxy framework: event types, define* factories, lifecycle hook signatures.",
5
5
  "keywords": [
6
6
  "moxxy",
@@ -24,11 +24,11 @@
24
24
  ],
25
25
  "homepage": "https://moxxy.ai",
26
26
  "bugs": {
27
- "url": "https://github.com/moxxy-ai/new_moxxy/issues"
27
+ "url": "https://github.com/moxxy-ai/moxxy/issues"
28
28
  },
29
29
  "repository": {
30
30
  "type": "git",
31
- "url": "git+https://github.com/moxxy-ai/new_moxxy.git",
31
+ "url": "git+https://github.com/moxxy-ai/moxxy.git",
32
32
  "directory": "packages/sdk"
33
33
  },
34
34
  "author": "Michal Makowski <michal.makowski97@gmail.com>",
@@ -54,21 +54,21 @@
54
54
  "publishConfig": {
55
55
  "access": "public"
56
56
  },
57
+ "peerDependencies": {
58
+ "zod": "^3.24.0"
59
+ },
60
+ "devDependencies": {
61
+ "typescript": "^5.7.3",
62
+ "vitest": "^2.1.8",
63
+ "zod": "^3.24.0",
64
+ "@moxxy/tsconfig": "0.0.0",
65
+ "@moxxy/vitest-preset": "0.0.0"
66
+ },
57
67
  "scripts": {
58
68
  "build": "tsc -p tsconfig.json",
59
69
  "typecheck": "tsc -p tsconfig.json --noEmit",
60
70
  "test": "vitest run",
61
71
  "test:watch": "vitest",
62
72
  "clean": "rm -rf dist .turbo"
63
- },
64
- "peerDependencies": {
65
- "zod": "catalog:"
66
- },
67
- "devDependencies": {
68
- "@moxxy/tsconfig": "workspace:*",
69
- "@moxxy/vitest-preset": "workspace:*",
70
- "typescript": "catalog:",
71
- "vitest": "catalog:",
72
- "zod": "catalog:"
73
73
  }
74
- }
74
+ }
@@ -4,6 +4,7 @@ import {
4
4
  asSessionId,
5
5
  asTurnId,
6
6
  estimateContextTokens,
7
+ isContextOverflowError,
7
8
  runCompactionIfNeeded,
8
9
  type CompactorDef,
9
10
  type EmittedEvent,
@@ -129,6 +130,46 @@ describe('runCompactionIfNeeded', () => {
129
130
  expect(ctx.emitted).toHaveLength(1);
130
131
  expect(ctx.emitted[0]).toMatchObject({ type: 'error', kind: 'retryable' });
131
132
  });
133
+
134
+ it('force compacts even when shouldCompact returns false', async () => {
135
+ const compact = vi.fn(async () => ({
136
+ type: 'compaction' as const,
137
+ replacedRange: [0, 1] as [number, number],
138
+ summary: 'summary',
139
+ tokensSaved: 500,
140
+ }));
141
+ const compactor: CompactorDef = {
142
+ name: 'gated',
143
+ shouldCompact: () => false, // gate says no…
144
+ compact,
145
+ };
146
+ const ctx = makeCtx({ compactor });
147
+ const did = await runCompactionIfNeeded(ctx, { force: true }); // …but force overrides
148
+ expect(did).toBe(true);
149
+ expect(compact).toHaveBeenCalledOnce();
150
+ expect(ctx.emitted.some((e) => e.type === 'compaction')).toBe(true);
151
+ });
152
+ });
153
+
154
+ describe('isContextOverflowError', () => {
155
+ it('matches common provider context-overflow phrasings', () => {
156
+ for (const msg of [
157
+ 'input exceeds context window',
158
+ "This model's maximum context length is 200000 tokens",
159
+ 'context_length_exceeded',
160
+ 'prompt is too long: 250000 tokens > 200000 maximum',
161
+ 'Please reduce the length of the messages',
162
+ 'too many input tokens',
163
+ ]) {
164
+ expect(isContextOverflowError(msg)).toBe(true);
165
+ }
166
+ });
167
+
168
+ it('does not match unrelated errors', () => {
169
+ for (const msg of ['rate limit exceeded', 'network timeout', '500 internal server error', 'invalid api key']) {
170
+ expect(isContextOverflowError(msg)).toBe(false);
171
+ }
172
+ });
132
173
  });
133
174
 
134
175
  interface MakeCtxOpts {
@@ -96,19 +96,22 @@ function safeJsonLen(v: unknown): number {
96
96
  * compactor bug can't kill the turn. Failures emit a non-fatal
97
97
  * `error` event for observability.
98
98
  */
99
- export async function runCompactionIfNeeded(ctx: ModeContext): Promise<void> {
99
+ export async function runCompactionIfNeeded(
100
+ ctx: ModeContext,
101
+ opts: { readonly force?: boolean } = {},
102
+ ): Promise<boolean> {
100
103
  const compactor = ctx.compactor;
101
- if (!compactor) return;
104
+ if (!compactor) return false;
102
105
 
103
106
  // Resolve the active model's descriptor so we use the *real* context
104
107
  // window. `Number.MAX_SAFE_INTEGER` (the old /compact behavior) made
105
108
  // threshold-based compactors always look comfortable, even at 99%.
106
109
  const descriptor = ctx.provider.models.find((m) => m.id === ctx.model);
107
110
  const contextWindow = descriptor?.contextWindow;
108
- if (!contextWindow || contextWindow <= 0) return;
111
+ if (!contextWindow || contextWindow <= 0) return false;
109
112
 
110
113
  const events = ctx.log.slice();
111
- if (events.length === 0) return;
114
+ if (events.length === 0) return false;
112
115
 
113
116
  const budget = {
114
117
  contextWindow,
@@ -116,21 +119,27 @@ export async function runCompactionIfNeeded(ctx: ModeContext): Promise<void> {
116
119
  reserveForOutput: descriptor?.maxOutputTokens ?? 0,
117
120
  } as const;
118
121
 
119
- let shouldRun = false;
120
- try {
121
- shouldRun = compactor.shouldCompact(ctx.log, budget);
122
- } catch (err) {
123
- await ctx.emit({
124
- type: 'error',
125
- sessionId: ctx.sessionId,
126
- turnId: ctx.turnId,
127
- source: 'system',
128
- kind: 'retryable',
129
- message: `compactor.shouldCompact threw: ${err instanceof Error ? err.message : String(err)}`,
130
- });
131
- return;
122
+ // `force` skips the threshold gate — used reactively after the provider
123
+ // rejects a request for being over the context window (our estimate can
124
+ // lag the provider's real tokenizer), so we compact and retry rather than
125
+ // failing the turn.
126
+ if (!opts.force) {
127
+ let shouldRun = false;
128
+ try {
129
+ shouldRun = compactor.shouldCompact(ctx.log, budget);
130
+ } catch (err) {
131
+ await ctx.emit({
132
+ type: 'error',
133
+ sessionId: ctx.sessionId,
134
+ turnId: ctx.turnId,
135
+ source: 'system',
136
+ kind: 'retryable',
137
+ message: `compactor.shouldCompact threw: ${err instanceof Error ? err.message : String(err)}`,
138
+ });
139
+ return false;
140
+ }
141
+ if (!shouldRun) return false;
132
142
  }
133
- if (!shouldRun) return;
134
143
 
135
144
  try {
136
145
  const result = await compactor.compact(events, {
@@ -138,7 +147,7 @@ export async function runCompactionIfNeeded(ctx: ModeContext): Promise<void> {
138
147
  budget,
139
148
  signal: ctx.signal,
140
149
  });
141
- if (result.tokensSaved <= 0 || result.summary.trim().length === 0) return;
150
+ if (result.tokensSaved <= 0 || result.summary.trim().length === 0) return false;
142
151
  // `compactor.compact` declares `Omit<CompactionEvent, keyof EventBase>`,
143
152
  // but every shipped compactor (and the SDK examples) fills sessionId /
144
153
  // turnId / source. Defensive-fill from ctx so a compactor that obeyed
@@ -150,6 +159,7 @@ export async function runCompactionIfNeeded(ctx: ModeContext): Promise<void> {
150
159
  ...result,
151
160
  } as EmittedEvent;
152
161
  await ctx.emit(emittable);
162
+ return true;
153
163
  } catch (err) {
154
164
  await ctx.emit({
155
165
  type: 'error',
@@ -159,5 +169,29 @@ export async function runCompactionIfNeeded(ctx: ModeContext): Promise<void> {
159
169
  kind: 'retryable',
160
170
  message: `compactor.compact threw: ${err instanceof Error ? err.message : String(err)}`,
161
171
  });
172
+ return false;
162
173
  }
163
174
  }
175
+
176
+ /**
177
+ * Heuristic: does this provider error mean "the request was too big for the
178
+ * model's context window"? Providers phrase it many ways (OpenAI "maximum
179
+ * context length is N tokens", Anthropic "prompt is too long", the runner's
180
+ * own "input exceeds context window"), and it usually arrives as a
181
+ * non-retryable 400 — so the turn loop matches on it to compact + retry
182
+ * instead of dying.
183
+ */
184
+ const CONTEXT_OVERFLOW_PATTERNS: ReadonlyArray<RegExp> = [
185
+ /context[\s_-]{0,2}(window|length)/i,
186
+ /maximum context/i,
187
+ /context_length_exceeded/i,
188
+ /exceeds?\b[^.]{0,24}context/i,
189
+ /input[^.]{0,24}(exceeds|too long|too large|too many)/i,
190
+ /too many (input )?tokens/i,
191
+ /prompt is too long/i,
192
+ /reduce the length/i,
193
+ ];
194
+
195
+ export function isContextOverflowError(message: string): boolean {
196
+ return CONTEXT_OVERFLOW_PATTERNS.some((re) => re.test(message));
197
+ }
package/src/define.ts CHANGED
@@ -3,6 +3,7 @@ import type { CacheStrategyDef } from './cache-strategy.js';
3
3
  import type { ChannelDef } from './channel.js';
4
4
  import type { CommandDef } from './command.js';
5
5
  import type { CompactorDef } from './compactor.js';
6
+ import type { EmbedderDef } from './embedding.js';
6
7
  import type { ModeDef } from './mode.js';
7
8
  import type { PermissionRule } from './permission.js';
8
9
  import type { Plugin, PluginSpec } from './plugin.js';
@@ -13,6 +14,7 @@ import type { ToolIsolationSpec } from './isolation.js';
13
14
  import type { TranscriberDef } from './transcriber.js';
14
15
  import type { ViewRendererDef } from './view-renderer.js';
15
16
  import type { TunnelProviderDef } from './tunnel.js';
17
+ import type { WorkflowExecutorDef } from './workflow.js';
16
18
  import type { z } from 'zod';
17
19
 
18
20
  export function definePlugin(spec: PluginSpec): Plugin {
@@ -104,6 +106,10 @@ export function defineTranscriber(spec: TranscriberDef): TranscriberDef {
104
106
  return Object.freeze(spec);
105
107
  }
106
108
 
109
+ export function defineEmbedder(spec: EmbedderDef): EmbedderDef {
110
+ return Object.freeze(spec);
111
+ }
112
+
107
113
  export function defineCommand(spec: CommandDef): CommandDef {
108
114
  return Object.freeze(spec);
109
115
  }
@@ -111,3 +117,7 @@ export function defineCommand(spec: CommandDef): CommandDef {
111
117
  export function defineAgent(spec: AgentDef): AgentDef {
112
118
  return Object.freeze(spec);
113
119
  }
120
+
121
+ export function defineWorkflowExecutor(spec: WorkflowExecutorDef): WorkflowExecutorDef {
122
+ return Object.freeze(spec);
123
+ }
package/src/embedding.ts CHANGED
@@ -20,3 +20,21 @@ export interface EmbeddingProvider {
20
20
  /** Embed a batch of strings. Returns vectors in input order. */
21
21
  embed(texts: ReadonlyArray<string>): Promise<ReadonlyArray<ReadonlyArray<number>>>;
22
22
  }
23
+
24
+ /**
25
+ * Plugin-side definition of an embedder. Mirrors `TranscriberDef` / `ProviderDef`:
26
+ * a `createClient(config)` factory the `EmbedderRegistry` calls when the user
27
+ * activates this embedder by name. Plugins contribute these via
28
+ * `PluginSpec.embedders`, so a user can install a new embedder package and
29
+ * select it by config without forking the host.
30
+ *
31
+ * `createClient` MUST be cheap — defer heavy model loading (onnx, transformers,
32
+ * network clients) into the returned provider's `embed()` so that merely
33
+ * registering a discovered embedder plugin never pulls its runtime in.
34
+ */
35
+ export interface EmbedderDef {
36
+ readonly name: string;
37
+ /** Optional human-readable label for UI surfaces. */
38
+ readonly displayName?: string;
39
+ createClient(config: Record<string, unknown>): EmbeddingProvider;
40
+ }
package/src/errors.ts CHANGED
@@ -49,6 +49,9 @@ export type MoxxyErrorCode =
49
49
  | 'CONFIG_INVALID'
50
50
  | 'PLUGIN_LOAD_FAILED'
51
51
  | 'UNKNOWN_COMMAND'
52
+ // --- Tool / runtime ---
53
+ | 'TOOL_ERROR' // a tool handler failed (bad input, not-found, exec error)
54
+ | 'ABORTED' // operation cancelled — turn abort signal or timeout kill
52
55
  // --- Catch-all ---
53
56
  | 'INTERNAL';
54
57
 
@@ -0,0 +1,61 @@
1
+ import { mkdtemp, readFile, readdir, stat } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { moxxyHome, moxxyPath, writeFileAtomic } from './fs-utils.js';
6
+
7
+ describe('writeFileAtomic', () => {
8
+ let dir: string;
9
+ beforeEach(async () => {
10
+ dir = await mkdtemp(join(tmpdir(), 'moxxy-fs-'));
11
+ });
12
+
13
+ it('writes content and creates missing parent dirs', async () => {
14
+ const target = join(dir, 'nested', 'deep', 'file.json');
15
+ await writeFileAtomic(target, '{"a":1}');
16
+ expect(await readFile(target, 'utf8')).toBe('{"a":1}');
17
+ });
18
+
19
+ it('overwrites an existing file and leaves no temp file behind', async () => {
20
+ const target = join(dir, 'file.txt');
21
+ await writeFileAtomic(target, 'first');
22
+ await writeFileAtomic(target, 'second');
23
+ expect(await readFile(target, 'utf8')).toBe('second');
24
+ const leftovers = (await readdir(dir)).filter((n) => n.includes('.tmp'));
25
+ expect(leftovers).toEqual([]);
26
+ });
27
+
28
+ it('enforces the requested mode past umask', async () => {
29
+ const target = join(dir, 'secret.json');
30
+ await writeFileAtomic(target, 'shh', { mode: 0o600 });
31
+ const mode = (await stat(target)).mode & 0o777;
32
+ expect(mode).toBe(0o600);
33
+ });
34
+
35
+ it('writes binary data unchanged', async () => {
36
+ const target = join(dir, 'bytes.bin');
37
+ const bytes = new Uint8Array([0, 1, 2, 255]);
38
+ await writeFileAtomic(target, bytes);
39
+ const read = await readFile(target);
40
+ expect(Array.from(read)).toEqual([0, 1, 2, 255]);
41
+ });
42
+ });
43
+
44
+ describe('moxxyHome / moxxyPath', () => {
45
+ const original = process.env.MOXXY_HOME;
46
+ afterEach(() => {
47
+ if (original === undefined) delete process.env.MOXXY_HOME;
48
+ else process.env.MOXXY_HOME = original;
49
+ });
50
+
51
+ it('honors MOXXY_HOME when set', () => {
52
+ process.env.MOXXY_HOME = '/custom/moxxy';
53
+ expect(moxxyHome()).toBe('/custom/moxxy');
54
+ expect(moxxyPath('vault.json')).toBe('/custom/moxxy/vault.json');
55
+ });
56
+
57
+ it('falls back to ~/.moxxy when unset', () => {
58
+ delete process.env.MOXXY_HOME;
59
+ expect(moxxyHome().endsWith('/.moxxy')).toBe(true);
60
+ });
61
+ });