@dna113p/machines 0.2.0 → 0.3.1

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 (79) hide show
  1. package/README.md +115 -11
  2. package/claude/statusline.mjs +277 -0
  3. package/dist/examples/chatgpt-web.d.ts +1 -0
  4. package/dist/examples/chatgpt-web.js +36 -0
  5. package/dist/examples/claude.d.ts +1 -0
  6. package/dist/examples/claude.js +30 -0
  7. package/dist/examples/codex.d.ts +1 -0
  8. package/dist/examples/codex.js +30 -0
  9. package/dist/examples/decision.d.ts +1 -0
  10. package/dist/examples/decision.js +66 -0
  11. package/dist/examples/deepseek.d.ts +1 -0
  12. package/dist/examples/deepseek.js +27 -0
  13. package/dist/mcp/server.d.ts +1 -0
  14. package/dist/mcp/server.js +74 -11
  15. package/dist/pi-extension/index.js +16 -9
  16. package/dist/src/acp.js +2 -1
  17. package/dist/src/agent-bindings.d.ts +6 -0
  18. package/dist/src/agent-bindings.js +35 -5
  19. package/dist/src/agent-limit.d.ts +25 -0
  20. package/dist/src/agent-limit.js +34 -0
  21. package/dist/src/agent-limits.d.ts +37 -0
  22. package/dist/src/agent-limits.js +212 -0
  23. package/dist/src/agent-protocol.js +3 -1
  24. package/dist/src/catalog.d.ts +3 -0
  25. package/dist/src/catalog.js +84 -12
  26. package/dist/src/chatgpt-web.d.ts +143 -0
  27. package/dist/src/chatgpt-web.js +946 -0
  28. package/dist/src/claude.d.ts +35 -0
  29. package/dist/src/claude.js +294 -0
  30. package/dist/src/cli.js +282 -16
  31. package/dist/src/codex.d.ts +16 -0
  32. package/dist/src/codex.js +176 -0
  33. package/dist/src/conversation.d.ts +74 -0
  34. package/dist/src/conversation.js +46 -0
  35. package/dist/src/coordinator-guidance.d.ts +3 -0
  36. package/dist/src/coordinator-guidance.js +17 -0
  37. package/dist/src/decision.d.ts +29 -0
  38. package/dist/src/decision.js +67 -0
  39. package/dist/src/deepseek.d.ts +12 -0
  40. package/dist/src/deepseek.js +16 -0
  41. package/dist/src/host.d.ts +11 -0
  42. package/dist/src/host.js +153 -16
  43. package/dist/src/index.d.ts +39 -2
  44. package/dist/src/index.js +62 -5
  45. package/dist/src/jev.d.ts +15 -0
  46. package/dist/src/jev.js +90 -0
  47. package/dist/src/json.d.ts +6 -0
  48. package/dist/src/json.js +28 -0
  49. package/dist/src/launcher.d.ts +11 -1
  50. package/dist/src/launcher.js +72 -3
  51. package/dist/src/laya.d.ts +15 -0
  52. package/dist/src/laya.js +13 -0
  53. package/dist/src/openrouter.d.ts +16 -0
  54. package/dist/src/openrouter.js +97 -0
  55. package/dist/src/run-inbox.d.ts +49 -0
  56. package/dist/src/run-inbox.js +233 -0
  57. package/dist/src/run-status.d.ts +121 -0
  58. package/dist/src/run-status.js +336 -0
  59. package/dist/src/run-watch.d.ts +24 -0
  60. package/dist/src/run-watch.js +112 -0
  61. package/dist/src/session.d.ts +15 -2
  62. package/dist/src/session.js +47 -8
  63. package/dist/src/system-one-http.d.ts +25 -0
  64. package/dist/src/system-one-http.js +146 -0
  65. package/dist/src/terminal-human.d.ts +8 -1
  66. package/dist/src/terminal-human.js +36 -8
  67. package/dist/src/von.d.ts +15 -0
  68. package/dist/src/von.js +13 -0
  69. package/docs/architecture.md +74 -3
  70. package/docs/authoring.md +809 -1
  71. package/docs/integrations.md +322 -3
  72. package/docs/local-decisions.md +212 -0
  73. package/package.json +21 -4
  74. package/services/laya/server.py +230 -0
  75. package/skills/machine-builder/SKILL.md +56 -6
  76. package/skills/machine-delegation/SKILL.md +128 -0
  77. package/skills/machine-delegation/references/delegation-machine.md +90 -0
  78. package/skills/machine-project-setup/SKILL.md +181 -0
  79. package/skills/machine-project-setup/references/project-workflows.md +50 -0
@@ -6,29 +6,50 @@ export interface Event {
6
6
  export type HumanOptions = {
7
7
  readonly choices: readonly string[];
8
8
  readonly suggestions?: never;
9
+ readonly discussion?: boolean;
9
10
  } | {
10
11
  readonly choices?: never;
11
12
  readonly suggestions?: readonly string[];
13
+ readonly discussion?: boolean;
12
14
  };
13
15
  export type HumanPrompt = string | (() => string);
14
16
  export interface HumanRequest {
17
+ /** A separate question channel; never an approval or a restricted choice. */
18
+ readonly discussion?: boolean;
15
19
  readonly prompt: string;
16
20
  readonly choices?: readonly string[];
17
21
  readonly suggestions?: readonly string[];
18
22
  }
19
- export type HumanRunner = (request: HumanRequest) => string | Promise<string>;
23
+ export type HumanResponse = string | {
24
+ readonly type: "question";
25
+ readonly text: string;
26
+ };
27
+ export type HumanRunner = (request: HumanRequest) => HumanResponse | Promise<HumanResponse>;
20
28
  export interface AgentOptions {
21
29
  readonly cwd?: string;
22
30
  readonly using?: string;
31
+ /**
32
+ * The transition this state takes when its Agent is at a usage limit. The
33
+ * event is delivered by the runtime and never offered to the Agent. A state
34
+ * that names it decides what a limit means: no fallback preset, no waiting.
35
+ */
36
+ readonly onLimit?: string;
23
37
  }
24
38
  export type AgentPrompt = string | (() => string);
25
39
  export interface AgentRequest {
26
40
  readonly prompt: string;
27
41
  readonly outcomes: readonly string[];
28
42
  readonly cwd?: string;
43
+ /**
44
+ * Set for a state that handles its own usage limit: a limit is to be thrown
45
+ * as it is found, not swapped away from or waited out. Runners that do
46
+ * neither ignore it.
47
+ */
48
+ readonly limits?: "report";
29
49
  }
30
50
  interface AgentInvocation extends AgentRequest {
31
51
  readonly using: string;
52
+ readonly onLimit?: string;
32
53
  }
33
54
  export type AgentUpdate = {
34
55
  readonly type: "identity";
@@ -43,8 +64,21 @@ export type AgentUpdate = {
43
64
  readonly id: string;
44
65
  readonly title?: string;
45
66
  readonly status?: "pending" | "in_progress" | "completed" | "failed";
67
+ } | {
68
+ /** A usage limit changed which preset runs this Agent, or made it wait. `text` is a ready one-line summary. */
69
+ readonly type: "limit";
70
+ readonly status: "swapped" | "waiting" | "resumed";
71
+ /** The limited preset. */
72
+ readonly agent: string;
73
+ /** The preset that continues the request. */
74
+ readonly to?: string;
75
+ /** ISO time the wait is expected to end. */
76
+ readonly until?: string;
77
+ readonly text: string;
46
78
  };
47
79
  export type AgentReporter = (update: AgentUpdate) => void;
80
+ export type { JsonValue } from "./json.ts";
81
+ export { AgentLimitError, agentLimitOf, type AgentLimit } from "./agent-limit.ts";
48
82
  export type AgentRunner = (request: AgentRequest, report?: AgentReporter) => Event | Promise<Event>;
49
83
  export interface RunOptions {
50
84
  readonly agents?: Readonly<Record<string, AgentRunner>>;
@@ -160,6 +194,7 @@ export declare function human<const TTransitions extends Readonly<Record<string,
160
194
  readonly src: "human";
161
195
  readonly input: () => {
162
196
  prompt: string;
197
+ discussion?: boolean | undefined;
163
198
  choices?: readonly string[] | undefined;
164
199
  suggestions?: readonly string[] | undefined;
165
200
  };
@@ -176,6 +211,7 @@ export declare function agent<const TTransitions extends Readonly<Record<string,
176
211
  readonly meta: {
177
212
  readonly "machines.agent": {
178
213
  readonly using: string;
214
+ readonly onLimit?: string | undefined;
179
215
  };
180
216
  };
181
217
  readonly invoke: {
@@ -185,6 +221,8 @@ export declare function agent<const TTransitions extends Readonly<Record<string,
185
221
  prompt: string;
186
222
  outcomes: string[];
187
223
  cwd?: string | undefined;
224
+ limits?: "report" | undefined;
225
+ onLimit?: string | undefined;
188
226
  };
189
227
  readonly onDone: {
190
228
  readonly actions: readonly [(args: unknown) => void, import("xstate").ActionFunction<any, any, Event, undefined, never, never, never, never, never>];
@@ -207,4 +245,3 @@ export interface MachinePrimitives {
207
245
  }
208
246
  export declare function requiredAgentNames(definition: AnyStateMachine): readonly string[];
209
247
  export declare function run<TMachine extends AnyStateMachine>(definition: TMachine, options?: RunOptions): Promise<SnapshotFrom<TMachine>>;
210
- export {};
package/dist/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { agentLimitOf } from "./agent-limit.js";
1
2
  import { terminalHuman } from "./terminal-human.js";
2
3
  import { createActor, fromPromise, raise, setup, } from "xstate";
4
+ export { AgentLimitError, agentLimitOf } from "./agent-limit.js";
3
5
  const machines = setup({
4
6
  actions: {
5
7
  humanInputFinished: () => { },
@@ -27,6 +29,9 @@ export function human(prompt, on, options = {}) {
27
29
  if (options.choices !== undefined && options.choices.length === 0) {
28
30
  throw new Error("Human choices must include at least one value");
29
31
  }
32
+ if (options.discussion && !Object.hasOwn(on, "question")) {
33
+ throw new Error("Discussion-enabled Human states must handle a question event");
34
+ }
30
35
  return {
31
36
  entry: "humanInputStarted",
32
37
  exit: "humanInputFinished",
@@ -34,6 +39,7 @@ export function human(prompt, on, options = {}) {
34
39
  src: "human",
35
40
  input: () => ({
36
41
  prompt: typeof prompt === "function" ? prompt() : prompt,
42
+ ...(options.discussion ? { discussion: true } : {}),
37
43
  ...(options.choices === undefined ? {} : { choices: options.choices }),
38
44
  ...(options.suggestions === undefined
39
45
  ? {}
@@ -49,17 +55,32 @@ export function agent(prompt, on, options = {}) {
49
55
  if (typeof using !== "string" || using.trim() === "" || using !== using.trim()) {
50
56
  throw new Error("Agent runner name must be a non-empty trimmed string");
51
57
  }
58
+ const onLimit = options.onLimit;
59
+ if (onLimit !== undefined) {
60
+ if (typeof onLimit !== "string" || onLimit.trim() === "" || onLimit !== onLimit.trim()) {
61
+ throw new Error("Agent onLimit must be a non-empty trimmed event name");
62
+ }
63
+ if (!Object.hasOwn(on, onLimit)) {
64
+ throw new Error(`Agent onLimit event "${onLimit}" must be one of the state's transitions`);
65
+ }
66
+ }
67
+ // The limit event comes from the runtime, so the Agent is never offered it.
68
+ const outcomes = Object.keys(on).filter((outcome) => outcome !== onLimit);
69
+ if (onLimit !== undefined && outcomes.length === 0) {
70
+ throw new Error(`Agent onLimit event "${onLimit}" cannot be the state's only transition: the Agent needs an outcome of its own`);
71
+ }
52
72
  return {
53
73
  meta: {
54
- [agentMetaKey]: { using },
74
+ [agentMetaKey]: { using, ...(onLimit === undefined ? {} : { onLimit }) },
55
75
  },
56
76
  invoke: {
57
77
  src: "agent",
58
78
  input: () => ({
59
79
  using,
60
80
  prompt: typeof prompt === "function" ? prompt() : prompt,
61
- outcomes: Object.keys(on),
81
+ outcomes,
62
82
  ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
83
+ ...(onLimit === undefined ? {} : { limits: "report", onLimit }),
63
84
  }),
64
85
  ...actorLifecycle("Agent", on),
65
86
  },
@@ -90,7 +111,11 @@ async function submittedHumanEvent(request, runner) {
90
111
  // Let onHumanInput(true) release terminal hotkeys before the runner takes stdin.
91
112
  const answer = await Promise.resolve().then(() => runner(request));
92
113
  if (typeof answer !== "string") {
93
- throw new Error("Human runner must return a string");
114
+ if (answer && request.discussion === true && answer.type === "question" &&
115
+ typeof answer.text === "string" && answer.text.trim() !== "" && answer.text.length <= 8000) {
116
+ return { type: "question", value: answer.text };
117
+ }
118
+ throw new Error("Human runner must return a string or an explicitly supported discussion question");
94
119
  }
95
120
  if (request.choices !== undefined && !request.choices.includes(answer)) {
96
121
  throw new Error(`Expected one of: ${request.choices.join(", ")}`);
@@ -121,12 +146,28 @@ export function run(definition, options = {}) {
121
146
  ? {}
122
147
  : {
123
148
  agent: fromPromise(async ({ input }) => {
124
- const { using, ...request } = input;
149
+ const { using, onLimit, ...request } = input;
125
150
  const runner = agentRunners[using];
126
151
  if (runner === undefined) {
127
152
  throw new Error(`Agent runner "${using}" was not supplied`);
128
153
  }
129
- return runner(request, options.onAgentUpdate);
154
+ if (onLimit === undefined)
155
+ return runner(request, options.onAgentUpdate);
156
+ let event;
157
+ try {
158
+ event = await runner(request, options.onAgentUpdate);
159
+ }
160
+ catch (cause) {
161
+ const limit = agentLimitOf(cause);
162
+ if (limit === undefined)
163
+ throw cause;
164
+ return limitEvent(onLimit, using, limit, cause);
165
+ }
166
+ // Only the runtime may say that the Agent is at a limit.
167
+ if (event !== null && typeof event === "object" && event.type === onLimit) {
168
+ throw new Error(`the runner returned "${onLimit}", which this state reserves for its usage-limit event`);
169
+ }
170
+ return event;
130
171
  }),
131
172
  }),
132
173
  },
@@ -150,6 +191,22 @@ export function run(definition, options = {}) {
150
191
  actor.start();
151
192
  });
152
193
  }
194
+ /** The event a state that names `onLimit` receives. It is plain JSON, so a Machine can keep or publish it. */
195
+ function limitEvent(type, role, limit, cause) {
196
+ const resets = typeof limit.resetsAt === "number" ? new Date(limit.resetsAt) : undefined;
197
+ return {
198
+ type,
199
+ limit: {
200
+ role,
201
+ ...(typeof limit.agent === "string" ? { agent: limit.agent } : {}),
202
+ harness: limit.harness,
203
+ ...(typeof limit.kind === "string" ? { kind: limit.kind } : {}),
204
+ // A reset outside the range of a date has no ISO form.
205
+ ...(resets === undefined || Number.isNaN(resets.getTime()) ? {} : { resetsAt: resets.toISOString() }),
206
+ },
207
+ message: cause instanceof Error ? cause.message : String(cause),
208
+ };
209
+ }
153
210
  function collectAgentNames(value, names) {
154
211
  if (value === null || typeof value !== "object")
155
212
  return;
@@ -0,0 +1,15 @@
1
+ import { type DecisionAgentOptions, type DecisionProvider } from "./decision.ts";
2
+ export interface JevProviderOptions {
3
+ /** Read lazily from TYPESAFE_API_KEY when omitted. Never used during discovery. */
4
+ readonly apiKey?: string;
5
+ /** Overrides JEV_MODEL, then the pinned default jev-1.13.0. */
6
+ readonly model?: string;
7
+ readonly timeoutMs?: number;
8
+ /** Transport injection for tests or an explicitly configured HTTP client. */
9
+ readonly fetch?: typeof globalThis.fetch;
10
+ }
11
+ export type JevAgentOptions = JevProviderOptions & DecisionAgentOptions;
12
+ /** TypeSafe Choice transport. No tool access, retries, or workflow policy. */
13
+ export declare function jevProvider(options?: JevProviderOptions): DecisionProvider;
14
+ /** Convenience preset; workflows can swap providers through decisionAgent instead. */
15
+ export declare function jevAgent(options?: JevAgentOptions): (request: import("./index.ts").AgentRequest, report?: import("./index.ts").AgentReporter) => Promise<import("./decision.ts").DecisionEvent>;
@@ -0,0 +1,90 @@
1
+ import * as v from "valibot";
2
+ import { decisionAgent } from "./decision.js";
3
+ const probability = v.pipe(v.number(), v.minValue(0), v.maxValue(1));
4
+ const responseSchema = v.object({
5
+ model: v.pipe(v.string(), v.check(value => value.trim().length > 0)),
6
+ answers: v.object({
7
+ decision: v.object({
8
+ type: v.literal("choice"),
9
+ choice: v.string(),
10
+ probabilities: v.record(v.string(), probability),
11
+ confidence: probability,
12
+ }),
13
+ }),
14
+ });
15
+ /** TypeSafe Choice transport. No tool access, retries, or workflow policy. */
16
+ export function jevProvider(options = {}) {
17
+ const timeoutMs = options.timeoutMs ?? 10_000;
18
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
19
+ throw new Error("Jev timeoutMs must be a positive 32-bit integer");
20
+ }
21
+ return {
22
+ name: "jev",
23
+ async decide(request) {
24
+ const apiKey = options.apiKey ?? process.env.TYPESAFE_API_KEY;
25
+ if (typeof apiKey !== "string" || !apiKey.trim()) {
26
+ throw new Error("Jev requires TYPESAFE_API_KEY or an explicit apiKey");
27
+ }
28
+ const model = options.model ?? process.env.JEV_MODEL ?? "jev-1.13.0";
29
+ if (typeof model !== "string" || !model.trim()) {
30
+ throw new Error("Jev model must be non-empty");
31
+ }
32
+ const count = Object.keys(request.choices).length;
33
+ if (count === 0 || count > 255) {
34
+ throw new Error("Jev Choice requires between 1 and 255 outcomes");
35
+ }
36
+ const controller = new AbortController();
37
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
38
+ const timeoutError = () => new Error(`Jev request timed out after ${timeoutMs}ms`);
39
+ try {
40
+ let response;
41
+ try {
42
+ response = await (options.fetch ?? globalThis.fetch)("https://api.typesafe.ai/v1/systemone", {
43
+ method: "POST",
44
+ redirect: "error",
45
+ signal: controller.signal,
46
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
47
+ body: JSON.stringify({
48
+ model,
49
+ state: request.input,
50
+ questions: {
51
+ decision: { type: "choice", instructions: request.question, criteria: request.choices },
52
+ },
53
+ }),
54
+ });
55
+ }
56
+ catch {
57
+ // Do not forward transport errors that might contain credentials or input.
58
+ throw controller.signal.aborted ? timeoutError() : new Error("Jev network request failed");
59
+ }
60
+ if (!response.ok) {
61
+ await response.body?.cancel().catch(() => { });
62
+ throw new Error(`Jev request failed (HTTP ${response.status}); no automatic retry`);
63
+ }
64
+ let body;
65
+ try {
66
+ body = await response.json();
67
+ }
68
+ catch {
69
+ throw controller.signal.aborted ? timeoutError() : new Error("Jev returned invalid JSON");
70
+ }
71
+ const parsed = v.safeParse(responseSchema, body);
72
+ if (!parsed.success)
73
+ throw new Error("Jev returned an invalid Choice response");
74
+ const { choice, probabilities, confidence } = parsed.output.answers.decision;
75
+ if (!Object.hasOwn(probabilities, choice)
76
+ || Object.values(probabilities).some(value => value > probabilities[choice])) {
77
+ throw new Error("Jev choice must have the highest probability");
78
+ }
79
+ return { choice, probabilities, confidence, model: parsed.output.model };
80
+ }
81
+ finally {
82
+ clearTimeout(timer);
83
+ }
84
+ },
85
+ };
86
+ }
87
+ /** Convenience preset; workflows can swap providers through decisionAgent instead. */
88
+ export function jevAgent(options = {}) {
89
+ return decisionAgent(jevProvider(options), options);
90
+ }
@@ -0,0 +1,6 @@
1
+ /** Values that can cross the hosted-run process boundary without conversion. */
2
+ export type JsonValue = null | boolean | number | string | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export declare function isJsonValue(value: unknown): value is JsonValue;
6
+ export declare function assertJsonValue(value: unknown, label?: string): asserts value is JsonValue;
@@ -0,0 +1,28 @@
1
+ export function isJsonValue(value) {
2
+ const ancestors = new Set();
3
+ function visit(item) {
4
+ if (item === null || typeof item === "string" || typeof item === "boolean")
5
+ return true;
6
+ if (typeof item === "number")
7
+ return Number.isFinite(item);
8
+ if (typeof item !== "object" || ancestors.has(item))
9
+ return false;
10
+ if (!Array.isArray(item) &&
11
+ Object.getPrototypeOf(item) !== Object.prototype &&
12
+ Object.getPrototypeOf(item) !== null)
13
+ return false;
14
+ if (Object.getOwnPropertySymbols(item).length > 0)
15
+ return false;
16
+ ancestors.add(item);
17
+ const valid = Array.isArray(item)
18
+ ? [...item].every(visit)
19
+ : Object.values(item).every(visit);
20
+ ancestors.delete(item);
21
+ return valid;
22
+ }
23
+ return visit(value);
24
+ }
25
+ export function assertJsonValue(value, label = "value") {
26
+ if (!isJsonValue(value))
27
+ throw new Error(`${label} must be a JSON-serializable value`);
28
+ }
@@ -1,11 +1,21 @@
1
1
  import type { AnyStateMachine, SnapshotFrom } from "xstate";
2
+ import { type JsonValue } from "./json.ts";
2
3
  import { type LauncherLocation, type MachineSummary, type AgentPresetSummary } from "./catalog.ts";
3
4
  export type { LauncherLocation, MachineSummary, AgentPresetSummary } from "./catalog.ts";
4
5
  import { type RunOptions } from "./index.ts";
5
6
  export interface PrepareMachineRunOptions extends LauncherLocation {
6
7
  readonly machine: string;
7
- readonly input?: string;
8
+ readonly input?: JsonValue;
8
9
  readonly agents?: Readonly<Record<string, string>>;
10
+ /**
11
+ * How this process waits at an Agent usage limit. `waitMinutes` overrides
12
+ * MACHINES_AGENT_LIMIT_WAIT_MINUTES; a hosted run reads the variable instead.
13
+ */
14
+ readonly agentLimits?: {
15
+ readonly waitMinutes?: number;
16
+ readonly now?: () => number;
17
+ readonly sleep?: (milliseconds: number) => Promise<void>;
18
+ };
9
19
  }
10
20
  export interface PreparedMachineRun {
11
21
  readonly name: string;
@@ -1,4 +1,7 @@
1
1
  import { basename, dirname, isAbsolute, resolve } from "node:path";
2
+ import { assertJsonValue } from "./json.js";
3
+ import { fallbackPresets } from "./agent-bindings.js";
4
+ import { createLimitMemory, defaultProbeMs, limitAwareRunner } from "./agent-limits.js";
2
5
  import { configuredAgentPresets } from "./catalog.js";
3
6
  import { queryCatalog } from "./discovery-worker.js";
4
7
  import { loadMachineModule, readAgentRoles, requireMachineDescription } from "./machine-module.js";
@@ -11,6 +14,8 @@ export async function listAgentPresets(location = {}) {
11
14
  return queryCatalog("agents", location);
12
15
  }
13
16
  export async function prepareMachineRun(options) {
17
+ if (options.input !== undefined)
18
+ assertJsonValue(options.input, "Machine input");
14
19
  const cwd = options.cwd ?? process.cwd();
15
20
  const home = options.home ?? machinesUserHome();
16
21
  const path = await resolveMachine(options.machine, cwd, home);
@@ -20,11 +25,14 @@ export async function prepareMachineRun(options) {
20
25
  if (typeof loaded.default !== "function") {
21
26
  throw new Error(`Machine "${path}" must default-export a factory`);
22
27
  }
23
- const definition = await loaded.default({ agent, final, human, machine, operation }, options.input ?? "");
28
+ const definition = await loaded.default({ agent, final, human, machine, operation }, options.input === undefined ? "" : options.input);
24
29
  const configured = await configuredAgentPresets(cwd, home);
25
30
  const usedAgentRoles = requiredAgentNames(definition);
26
- const agents = applyAgentOverrides(path, configured.agents, configured.presets, new Set([...usedAgentRoles, ...Object.keys(agentRoles)]), options.agents ?? {});
31
+ const requiredRoles = new Set([...usedAgentRoles, ...Object.keys(agentRoles)]);
32
+ const overrides = options.agents ?? {};
33
+ const agents = applyAgentOverrides(path, configured.agents, configured.presets, requiredRoles, overrides);
27
34
  preflightAgentRoles(path, usedAgentRoles, agentRoles, agents);
35
+ const limitAware = limitAwareAgents(requiredRoles, overrides, configured, limitPolicy(options.agentLimits));
28
36
  let started = false;
29
37
  return {
30
38
  name: machineName(path),
@@ -35,7 +43,7 @@ export async function prepareMachineRun(options) {
35
43
  if (started)
36
44
  throw new Error(`Prepared Machine "${path}" has already started`);
37
45
  started = true;
38
- return run(definition, { ...runOptions, agents });
46
+ return run(definition, { ...runOptions, agents: { ...agents, ...limitAware } });
39
47
  },
40
48
  };
41
49
  }
@@ -71,6 +79,67 @@ function applyAgentOverrides(machinePath, agents, presets, requiredRoles, overri
71
79
  }
72
80
  return resolved;
73
81
  }
82
+ /**
83
+ * Wraps each role the Machine uses or declares so that a usage limit swaps to
84
+ * the bound preset's fallbacks or waits for a reset. The roles share one memory
85
+ * of limited presets, kept only for this prepared run.
86
+ */
87
+ function limitAwareAgents(roles, overrides, configured, policy) {
88
+ const swapping = fallbackEnabled();
89
+ const memory = createLimitMemory();
90
+ const wrapped = {};
91
+ for (const role of roles) {
92
+ const bound = Object.hasOwn(overrides, role) ? overrides[role] ?? role : role;
93
+ // With swapping off a role still waits for its own preset's reset, and names no other preset.
94
+ const fallback = swapping
95
+ ? fallbackPresets(bound, presetNamed(configured.presets, bound) ?? {}, configured.fallback) ?? []
96
+ : [];
97
+ const candidates = [bound, ...fallback].map((name) => {
98
+ const preset = presetNamed(configured.presets, name);
99
+ if (preset === undefined) {
100
+ const available = Object.keys(configured.presets).sort().join(", ");
101
+ throw new Error(`Agent preset "${bound}" for role "${role}" falls back to "${name}", which is not a configured preset. Available presets: ${available}`);
102
+ }
103
+ return { name, runner: preset.runner, ...(preset.harness === undefined ? {} : { harness: preset.harness }) };
104
+ });
105
+ wrapped[role] = limitAwareRunner(role, candidates, memory, policy);
106
+ }
107
+ return wrapped;
108
+ }
109
+ function presetNamed(presets, name) {
110
+ return Object.hasOwn(presets, name) ? presets[name] : undefined;
111
+ }
112
+ function fallbackEnabled() {
113
+ const configured = process.env.MACHINES_AGENT_FALLBACK;
114
+ if (configured === undefined || configured === "" || configured === "on")
115
+ return true;
116
+ if (configured === "off")
117
+ return false;
118
+ throw new Error(`MACHINES_AGENT_FALLBACK must be "off" or "on", not "${configured}"`);
119
+ }
120
+ const defaultLimitWaitMinutes = 360;
121
+ function limitPolicy(options = {}) {
122
+ const configured = process.env.MACHINES_AGENT_LIMIT_WAIT_MINUTES;
123
+ let minutes = defaultLimitWaitMinutes;
124
+ if (options.waitMinutes !== undefined) {
125
+ if (!Number.isFinite(options.waitMinutes) || options.waitMinutes < 0) {
126
+ throw new Error("agentLimits.waitMinutes must be a non-negative number of minutes");
127
+ }
128
+ minutes = options.waitMinutes;
129
+ }
130
+ else if (configured !== undefined && configured !== "") {
131
+ if (!/^\d+(?:\.\d+)?$/u.test(configured)) {
132
+ throw new Error(`MACHINES_AGENT_LIMIT_WAIT_MINUTES must be a non-negative number of minutes, not "${configured}"`);
133
+ }
134
+ minutes = Number(configured);
135
+ }
136
+ return {
137
+ maxWaitMs: minutes * 60_000,
138
+ probeMs: defaultProbeMs,
139
+ ...(options.now === undefined ? {} : { now: options.now }),
140
+ ...(options.sleep === undefined ? {} : { sleep: options.sleep }),
141
+ };
142
+ }
74
143
  function preflightAgentRoles(machinePath, usedNames, declaredRoles, agents) {
75
144
  const undeclared = usedNames.filter((name) => name !== "default" && !Object.hasOwn(declaredRoles, name));
76
145
  if (undeclared.length > 0) {
@@ -0,0 +1,15 @@
1
+ import { type DecisionAgentOptions, type DecisionProvider } from "./decision.ts";
2
+ import { type SystemOneHttpOptions } from "./system-one-http.ts";
3
+ export interface LayaProviderOptions extends SystemOneHttpOptions {
4
+ /** Overrides LAYA_BASE_URL, then http://127.0.0.1:8001. */
5
+ readonly baseUrl?: string;
6
+ /** Overrides LAYA_API_KEY. No other provider's credentials are used. */
7
+ readonly apiKey?: string;
8
+ /** Overrides LAYA_MODEL, then laya. Must match the bridge's --model identifier. */
9
+ readonly model?: string;
10
+ }
11
+ export type LayaAgentOptions = LayaProviderOptions & DecisionAgentOptions;
12
+ /** Choice adapter for services/laya/server.py; the server owns checkpoint loading. */
13
+ export declare function layaProvider(options?: LayaProviderOptions): DecisionProvider;
14
+ /** Convenience binding. The initial Laya checkpoint needs at least two choices. */
15
+ export declare function layaAgent(options?: LayaAgentOptions): (request: import("./index.ts").AgentRequest, report?: import("./index.ts").AgentReporter) => Promise<import("./decision.ts").DecisionEvent>;
@@ -0,0 +1,13 @@
1
+ import { decisionAgent } from "./decision.js";
2
+ import { systemOneHttpProvider } from "./system-one-http.js";
3
+ /** Choice adapter for services/laya/server.py; the server owns checkpoint loading. */
4
+ export function layaProvider(options = {}) {
5
+ return systemOneHttpProvider({
6
+ name: "laya", label: "Laya", envPrefix: "LAYA",
7
+ baseUrl: "http://127.0.0.1:8001", model: "laya", minChoices: 2,
8
+ }, options);
9
+ }
10
+ /** Convenience binding. The initial Laya checkpoint needs at least two choices. */
11
+ export function layaAgent(options = {}) {
12
+ return decisionAgent(layaProvider(options), options);
13
+ }
@@ -0,0 +1,16 @@
1
+ import { type DecisionAgentOptions, type DecisionProvider } from "./decision.ts";
2
+ export interface OpenRouterDecisionProviderOptions {
3
+ /** Read lazily from OPENROUTER_API_KEY. Never needed during discovery. */
4
+ readonly apiKey?: string;
5
+ /** Overrides OPENROUTER_DECISION_MODEL, then the pinned typesafe/jev-1.13. */
6
+ readonly model?: string;
7
+ /** Defaults to 10 seconds; covers both the request and response-body read. */
8
+ readonly timeoutMs?: number;
9
+ /** Explicit transport injection. Custom clients must honor the AbortSignal. */
10
+ readonly fetch?: typeof globalThis.fetch;
11
+ }
12
+ export type OpenRouterDecisionAgentOptions = OpenRouterDecisionProviderOptions & DecisionAgentOptions;
13
+ /** OpenRouter Decisions transport, not chat completions or a coding agent. */
14
+ export declare function openRouterDecisionProvider(options?: OpenRouterDecisionProviderOptions): DecisionProvider;
15
+ /** Convenience preset; workflow policy stays in the Machine. */
16
+ export declare function openRouterDecisionAgent(options?: OpenRouterDecisionAgentOptions): (request: import("./index.ts").AgentRequest, report?: import("./index.ts").AgentReporter) => Promise<import("./decision.ts").DecisionEvent>;
@@ -0,0 +1,97 @@
1
+ import * as v from "valibot";
2
+ import { decisionAgent } from "./decision.js";
3
+ const nonBlank = v.pipe(v.string(), v.check(value => value.trim().length > 0));
4
+ const probability = v.pipe(v.number(), v.minValue(0), v.maxValue(1));
5
+ // Validate only the fields consumed by this adapter. Usage and other metadata
6
+ // are not forwarded into workflow events or observer output.
7
+ const responseSchema = v.object({
8
+ model: nonBlank,
9
+ answers: v.object({
10
+ decision: v.object({
11
+ type: v.literal("choice"),
12
+ choice: nonBlank,
13
+ probabilities: v.optional(v.pipe(v.unknown(), v.check(value => !Array.isArray(value)), v.record(v.string(), probability))),
14
+ confidence: v.optional(probability),
15
+ }),
16
+ }),
17
+ });
18
+ /** OpenRouter Decisions transport, not chat completions or a coding agent. */
19
+ export function openRouterDecisionProvider(options = {}) {
20
+ const timeoutMs = options.timeoutMs ?? 10_000;
21
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
22
+ throw new Error("OpenRouter Decisions timeoutMs must be a positive 32-bit integer");
23
+ }
24
+ return {
25
+ name: "openrouter-decision",
26
+ async decide(request) {
27
+ const apiKey = options.apiKey ?? process.env.OPENROUTER_API_KEY;
28
+ if (typeof apiKey !== "string" || !apiKey.trim()) {
29
+ throw new Error("OpenRouter Decisions requires OPENROUTER_API_KEY or an explicit apiKey");
30
+ }
31
+ const model = options.model ?? process.env.OPENROUTER_DECISION_MODEL ?? "typesafe/jev-1.13";
32
+ if (typeof model !== "string" || !model.trim()) {
33
+ throw new Error("OpenRouter Decisions model must be non-empty");
34
+ }
35
+ if (Object.keys(request.choices).length === 0) {
36
+ throw new Error("OpenRouter Decisions requires at least one outcome");
37
+ }
38
+ // OpenRouter requires string criteria; the neutral contract allows null.
39
+ // Preserve labels verbatim and use the label when no description is given.
40
+ const criteria = Object.fromEntries(Object.entries(request.choices).map(([label, description]) => [label, description ?? label]));
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
43
+ const timeoutError = () => new Error(`OpenRouter Decisions request timed out after ${timeoutMs}ms`);
44
+ try {
45
+ let response;
46
+ try {
47
+ response = await (options.fetch ?? globalThis.fetch)("https://openrouter.ai/api/alpha/decisions", {
48
+ method: "POST",
49
+ redirect: "error",
50
+ signal: controller.signal,
51
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
52
+ body: JSON.stringify({
53
+ model,
54
+ state: request.input,
55
+ questions: {
56
+ decision: { type: "choice", instructions: request.question, criteria },
57
+ },
58
+ }),
59
+ });
60
+ }
61
+ catch {
62
+ // Transport errors may contain credentials or evidence; do not expose them.
63
+ throw controller.signal.aborted ? timeoutError() : new Error("OpenRouter Decisions network request failed");
64
+ }
65
+ if (!response.ok) {
66
+ // Discard diagnostics without waiting on an untrusted error-body stream.
67
+ void response.body?.cancel().catch(() => { });
68
+ throw new Error(`OpenRouter Decisions request failed (HTTP ${response.status}); no automatic retry`);
69
+ }
70
+ let body;
71
+ try {
72
+ body = await response.json();
73
+ }
74
+ catch {
75
+ throw controller.signal.aborted ? timeoutError() : new Error("OpenRouter Decisions returned invalid JSON");
76
+ }
77
+ const parsed = v.safeParse(responseSchema, body);
78
+ if (!parsed.success)
79
+ throw new Error("OpenRouter Decisions returned an invalid Choice response");
80
+ const { choice, probabilities, confidence } = parsed.output.answers.decision;
81
+ return {
82
+ choice,
83
+ ...(probabilities === undefined ? {} : { probabilities }),
84
+ ...(confidence === undefined ? {} : { confidence }),
85
+ model: parsed.output.model,
86
+ };
87
+ }
88
+ finally {
89
+ clearTimeout(timer);
90
+ }
91
+ },
92
+ };
93
+ }
94
+ /** Convenience preset; workflow policy stays in the Machine. */
95
+ export function openRouterDecisionAgent(options = {}) {
96
+ return decisionAgent(openRouterDecisionProvider(options), options);
97
+ }