@dna113p/machines 0.2.0 → 0.3.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.
Files changed (77) hide show
  1. package/README.md +97 -4
  2. package/claude/statusline.mjs +244 -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 +72 -10
  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 +194 -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 +3 -0
  42. package/dist/src/host.js +68 -7
  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 +229 -0
  57. package/dist/src/run-status.d.ts +97 -0
  58. package/dist/src/run-status.js +285 -0
  59. package/dist/src/session.d.ts +13 -0
  60. package/dist/src/session.js +41 -3
  61. package/dist/src/system-one-http.d.ts +25 -0
  62. package/dist/src/system-one-http.js +146 -0
  63. package/dist/src/terminal-human.d.ts +8 -1
  64. package/dist/src/terminal-human.js +36 -8
  65. package/dist/src/von.d.ts +15 -0
  66. package/dist/src/von.js +13 -0
  67. package/docs/architecture.md +67 -2
  68. package/docs/authoring.md +809 -1
  69. package/docs/integrations.md +243 -3
  70. package/docs/local-decisions.md +212 -0
  71. package/package.json +21 -4
  72. package/services/laya/server.py +230 -0
  73. package/skills/machine-builder/SKILL.md +56 -6
  74. package/skills/machine-delegation/SKILL.md +123 -0
  75. package/skills/machine-delegation/references/delegation-machine.md +90 -0
  76. package/skills/machine-project-setup/SKILL.md +181 -0
  77. package/skills/machine-project-setup/references/project-workflows.md +50 -0
@@ -1,4 +1,5 @@
1
1
  import type { StateValue } from "xstate";
2
+ import type { JsonValue } from "./json.ts";
2
3
  import { type HostedHumanRequest } from "./host.ts";
3
4
  import type { AgentUpdate } from "./index.ts";
4
5
  import type { PrepareMachineRunOptions } from "./launcher.ts";
@@ -11,12 +12,18 @@ export interface RunSnapshot {
11
12
  readonly cwd: string;
12
13
  readonly status: RunStatus;
13
14
  readonly state?: StateValue;
15
+ readonly output?: JsonValue;
14
16
  readonly startedAt: string;
15
17
  readonly updatedAt: string;
16
18
  readonly elapsedSeconds: number;
17
19
  readonly agent?: Extract<AgentUpdate, {
18
20
  type: "identity";
19
21
  }>;
22
+ /** Present while the current Agent state waits for a usage limit to reset; `status` stays `running`. */
23
+ readonly limit?: {
24
+ readonly text: string;
25
+ readonly until?: string;
26
+ };
20
27
  readonly human?: HostedHumanRequest;
21
28
  readonly error?: string;
22
29
  }
@@ -35,6 +42,12 @@ export declare class MachineSession {
35
42
  readonly requestId: string;
36
43
  readonly response: string;
37
44
  }): Promise<RunSnapshot>;
45
+ /** Sends a discussion question in place of an answer; the Machine then asks again as a new request. */
46
+ ask(input: {
47
+ readonly runId: string;
48
+ readonly requestId: string;
49
+ readonly question: string;
50
+ }): Promise<RunSnapshot>;
38
51
  close(): void;
39
52
  }
40
53
  export declare function isActive(status: RunStatus): boolean;
@@ -31,13 +31,23 @@ export class MachineSession {
31
31
  ...options,
32
32
  signal: cancellation.signal,
33
33
  onState: (state) => update("updated", (run) => {
34
- if (JSON.stringify(run.state) !== JSON.stringify(state))
34
+ if (JSON.stringify(run.state) !== JSON.stringify(state)) {
35
35
  run.agent = undefined;
36
+ run.limit = undefined;
37
+ }
36
38
  run.state = state;
37
39
  }),
38
40
  onAgentUpdate: (agent) => {
39
41
  if (agent.type === "identity")
40
42
  update("updated", (run) => { run.agent = agent; });
43
+ // A swap while every preset is being tried again leaves the wait as it is shown.
44
+ else if (agent.type === "limit" && agent.status !== "swapped") {
45
+ update("updated", (run) => {
46
+ run.limit = agent.status === "resumed"
47
+ ? undefined
48
+ : { text: agent.text, ...(agent.until === undefined ? {} : { until: agent.until }) };
49
+ });
50
+ }
41
51
  },
42
52
  onHumanRequest: (request) => update("human", (run) => {
43
53
  run.status = "waiting";
@@ -61,7 +71,7 @@ export class MachineSession {
61
71
  status: "running",
62
72
  };
63
73
  this.#runs.set(id, run);
64
- void host.result.then(({ state }) => this.#finish(run, { state }), (cause) => this.#finish(run, { error: errorMessage(cause) }));
74
+ void host.result.then((result) => this.#finish(run, result), (cause) => this.#finish(run, { error: errorMessage(cause) }));
65
75
  this.#emit("updated", run);
66
76
  return snapshot(run);
67
77
  }
@@ -92,6 +102,29 @@ export class MachineSession {
92
102
  await sent;
93
103
  return snapshot(run);
94
104
  }
105
+ /** Sends a discussion question in place of an answer; the Machine then asks again as a new request. */
106
+ async ask(input) {
107
+ const run = this.#requireRun(input.runId);
108
+ if (run.status !== "waiting" || run.human === undefined) {
109
+ throw new Error(`Machine run "${input.runId}" is not waiting for Human input`);
110
+ }
111
+ if (run.human.requestId !== input.requestId) {
112
+ throw new Error("Machine Human request is stale; read the current requestId");
113
+ }
114
+ if (run.human.discussion !== true)
115
+ throw new Error("This Human request does not support discussion");
116
+ if (input.question.trim() === "" || input.question.length > 8000) {
117
+ throw new Error("Question must contain 1–8000 characters");
118
+ }
119
+ // Claimed as a response is: the request is spent once its question is on the way.
120
+ run.status = "running";
121
+ run.human = undefined;
122
+ run.updatedAt = Date.now();
123
+ const sent = run.host.ask(input.question, input.requestId);
124
+ this.#emit("updated", run);
125
+ await sent;
126
+ return snapshot(run);
127
+ }
95
128
  close() {
96
129
  if (this.#closed)
97
130
  return;
@@ -114,12 +147,15 @@ export class MachineSession {
114
147
  if (this.#closed || !isActive(run.status))
115
148
  return;
116
149
  run.status = "state" in outcome ? "completed" : "failed";
117
- if ("state" in outcome)
150
+ if ("state" in outcome) {
118
151
  run.state = outcome.state;
152
+ run.output = outcome.output;
153
+ }
119
154
  else
120
155
  run.error = outcome.error;
121
156
  run.human = undefined;
122
157
  run.agent = undefined;
158
+ run.limit = undefined;
123
159
  run.updatedAt = Date.now();
124
160
  const terminal = [...this.#runs.values()]
125
161
  .filter((item) => !isActive(item.status))
@@ -152,10 +188,12 @@ function snapshot(run) {
152
188
  cwd: run.cwd,
153
189
  status: run.status,
154
190
  ...(run.state === undefined ? {} : { state: structuredClone(run.state) }),
191
+ ...(run.output === undefined ? {} : { output: structuredClone(run.output) }),
155
192
  startedAt: new Date(run.startedAt).toISOString(),
156
193
  updatedAt: new Date(run.updatedAt).toISOString(),
157
194
  elapsedSeconds: Math.floor(((isActive(run.status) ? Date.now() : run.updatedAt) - run.startedAt) / 1_000),
158
195
  ...(run.agent === undefined ? {} : { agent: { ...run.agent } }),
196
+ ...(run.limit === undefined ? {} : { limit: { ...run.limit } }),
159
197
  ...(run.human === undefined ? {} : { human: structuredClone(run.human) }),
160
198
  ...(run.error === undefined ? {} : { error: run.error }),
161
199
  };
@@ -0,0 +1,25 @@
1
+ import type { DecisionProvider } from "./decision.ts";
2
+ /** Shared transport options for self-hosted System One Choice services. */
3
+ export interface SystemOneHttpOptions {
4
+ /** Server root (optionally including a proxy prefix), without /v1/systemone. */
5
+ readonly baseUrl?: string;
6
+ /** Optional Bearer token. Only the selected provider's environment is read. */
7
+ readonly apiKey?: string;
8
+ /** Wire model identifier; checkpoint/backend selection belongs to the server. */
9
+ readonly model?: string;
10
+ /** Defaults to 30 seconds, including reading the response body. */
11
+ readonly timeoutMs?: number;
12
+ /** Injected clients must honor the AbortSignal to release network resources. */
13
+ readonly fetch?: typeof globalThis.fetch;
14
+ }
15
+ interface Service {
16
+ readonly name: string;
17
+ readonly label: string;
18
+ readonly envPrefix: string;
19
+ readonly baseUrl: string;
20
+ readonly model: string;
21
+ readonly minChoices: number;
22
+ }
23
+ /** Internal plumbing shared by Von and Laya, not a new workflow primitive. */
24
+ export declare function systemOneHttpProvider(service: Service, options: SystemOneHttpOptions): DecisionProvider;
25
+ export {};
@@ -0,0 +1,146 @@
1
+ import * as v from "valibot";
2
+ const nonBlank = v.pipe(v.string(), v.check(value => value.trim().length > 0));
3
+ const probability = v.pipe(v.number(), v.minValue(0), v.maxValue(1));
4
+ const responseSchema = v.object({
5
+ model: nonBlank,
6
+ answers: v.object({
7
+ decision: v.object({
8
+ type: v.literal("choice"),
9
+ choice: nonBlank,
10
+ probabilities: v.pipe(v.unknown(), v.check(value => !Array.isArray(value)), v.record(v.string(), probability)),
11
+ confidence: probability,
12
+ }),
13
+ }),
14
+ });
15
+ const maxResponseBytes = 1_048_576;
16
+ /** Internal plumbing shared by Von and Laya, not a new workflow primitive. */
17
+ export function systemOneHttpProvider(service, options) {
18
+ const { label, envPrefix } = service;
19
+ const timeoutMs = options.timeoutMs ?? 30_000;
20
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
21
+ throw new Error(`${label} timeoutMs must be a positive 32-bit integer`);
22
+ }
23
+ return {
24
+ name: service.name,
25
+ async decide(request) {
26
+ // Resolve all environment configuration lazily: discovery never contacts a
27
+ // service, validates credentials, starts Python, or downloads model weights.
28
+ const endpoint = serviceEndpoint(options.baseUrl ?? process.env[`${envPrefix}_BASE_URL`] ?? service.baseUrl, label);
29
+ const apiKey = options.apiKey ?? process.env[`${envPrefix}_API_KEY`];
30
+ if (apiKey !== undefined && (typeof apiKey !== "string" || !/^[\x21-\x7e]+$/.test(apiKey))) {
31
+ throw new Error(`${label} apiKey must be a non-empty ASCII Bearer token`);
32
+ }
33
+ const model = options.model ?? process.env[`${envPrefix}_MODEL`] ?? service.model;
34
+ if (typeof model !== "string" || !model.trim())
35
+ throw new Error(`${label} model must be non-empty`);
36
+ const choices = Object.entries(request.choices);
37
+ if (choices.length < service.minChoices) {
38
+ throw new Error(`${label} Choice requires at least ${service.minChoices} outcome(s)`);
39
+ }
40
+ if (!request.question?.trim() || !request.input?.trim()
41
+ || choices.some(([key, value]) => !key.trim() || key !== key.trim()
42
+ || (value !== null && (typeof value !== "string" || !value.trim())))) {
43
+ throw new Error(`${label} requires non-empty evidence, question, and valid choices`);
44
+ }
45
+ const controller = new AbortController();
46
+ const timeoutError = () => new Error(`${label} request timed out after ${timeoutMs}ms`);
47
+ let timer;
48
+ const timeout = new Promise((_resolve, reject) => {
49
+ timer = setTimeout(() => { controller.abort(); reject(timeoutError()); }, timeoutMs);
50
+ });
51
+ const execute = async () => {
52
+ let response;
53
+ try {
54
+ response = await (options.fetch ?? globalThis.fetch)(endpoint, {
55
+ method: "POST", redirect: "error", signal: controller.signal,
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ ...(apiKey === undefined ? {} : { Authorization: `Bearer ${apiKey}` }),
59
+ },
60
+ body: JSON.stringify({
61
+ model, state: request.input,
62
+ questions: {
63
+ decision: { type: "choice", instructions: request.question, criteria: request.choices },
64
+ },
65
+ }),
66
+ });
67
+ }
68
+ catch {
69
+ throw controller.signal.aborted ? timeoutError() : new Error(`${label} network request failed`);
70
+ }
71
+ if (controller.signal.aborted || !response.ok) {
72
+ // Do not wait on untrusted error streams, or expose their contents.
73
+ void response.body?.cancel().catch(() => { });
74
+ if (controller.signal.aborted)
75
+ throw timeoutError();
76
+ throw new Error(`${label} request failed (HTTP ${response.status}); no automatic retry`);
77
+ }
78
+ let body;
79
+ const reader = response.body?.getReader();
80
+ const cancel = () => { void reader?.cancel().catch(() => { }); };
81
+ controller.signal.addEventListener("abort", cancel, { once: true });
82
+ try {
83
+ const chunks = [];
84
+ let bytes = 0;
85
+ if (reader) {
86
+ while (true) {
87
+ const { value, done } = await reader.read();
88
+ if (done)
89
+ break;
90
+ bytes += value.byteLength;
91
+ if (bytes > maxResponseBytes) {
92
+ cancel();
93
+ throw new RangeError("Response size limit");
94
+ }
95
+ chunks.push(value);
96
+ }
97
+ }
98
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
99
+ }
100
+ catch (error) {
101
+ if (controller.signal.aborted)
102
+ throw timeoutError();
103
+ throw new Error(error instanceof RangeError
104
+ ? `${label} response exceeds 1 MiB` : `${label} returned invalid JSON`);
105
+ }
106
+ finally {
107
+ controller.signal.removeEventListener("abort", cancel);
108
+ reader?.releaseLock();
109
+ }
110
+ if (controller.signal.aborted)
111
+ throw timeoutError();
112
+ const parsed = v.safeParse(responseSchema, body);
113
+ if (!parsed.success)
114
+ throw new Error(`${label} returned an invalid Choice response`);
115
+ const { choice, probabilities, confidence } = parsed.output.answers.decision;
116
+ if (!Object.hasOwn(probabilities, choice)
117
+ || Object.values(probabilities).some(value => value > probabilities[choice])) {
118
+ throw new Error(`${label} choice must have the highest probability`);
119
+ }
120
+ return { choice, probabilities, confidence, model: parsed.output.model };
121
+ };
122
+ try {
123
+ return await Promise.race([execute(), timeout]);
124
+ }
125
+ finally {
126
+ clearTimeout(timer);
127
+ }
128
+ },
129
+ };
130
+ }
131
+ function serviceEndpoint(value, label) {
132
+ try {
133
+ if (typeof value !== "string" || !value.trim() || value !== value.trim())
134
+ throw new Error();
135
+ const url = new URL(value);
136
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
137
+ throw new Error();
138
+ }
139
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/v1/systemone`;
140
+ return url.href;
141
+ }
142
+ catch {
143
+ // A malformed URL may contain a password; never include it in the error.
144
+ throw new Error(`${label} baseUrl must be an HTTP(S) server root without credentials, query, or fragment`);
145
+ }
146
+ }
@@ -1,2 +1,9 @@
1
1
  import type { HumanRequest } from "./index.ts";
2
- export declare function terminalHuman(request: HumanRequest): Promise<string>;
2
+ export declare class TerminalInputClosed extends Error {
3
+ constructor();
4
+ }
5
+ /**
6
+ * Asks on the process's own terminal. Aborting the signal withdraws the
7
+ * question, restores the terminal, and rejects with the signal's reason.
8
+ */
9
+ export declare function terminalHuman(request: HumanRequest, signal?: AbortSignal): Promise<string>;
@@ -1,27 +1,47 @@
1
1
  import { clearScreenDown, createInterface, cursorTo, emitKeypressEvents, moveCursor, } from "node:readline";
2
2
  import { cyan, dim, green } from "yoctocolors";
3
- export function terminalHuman(request) {
3
+ export class TerminalInputClosed extends Error {
4
+ constructor() {
5
+ super("Terminal input closed before a response");
6
+ }
7
+ }
8
+ /**
9
+ * Asks on the process's own terminal. Aborting the signal withdraws the
10
+ * question, restores the terminal, and rejects with the signal's reason.
11
+ */
12
+ export function terminalHuman(request, signal) {
13
+ if (signal?.aborted)
14
+ return Promise.reject(signal.reason);
4
15
  const values = request.choices ?? request.suggestions ?? [];
5
16
  const allowOther = request.suggestions !== undefined;
6
17
  if (values.length > 0
7
18
  && process.stdin.isTTY
8
19
  && process.stdout.isTTY
9
20
  && typeof process.stdin.setRawMode === "function") {
10
- return selectHumanInput(request.prompt, values, allowOther);
21
+ return selectHumanInput(request.prompt, values, allowOther, signal);
11
22
  }
12
- return readFreeformInput(request.prompt, values, allowOther ? "Suggestions" : "Choices");
23
+ return readFreeformInput(request.prompt, values, allowOther ? "Suggestions" : "Choices", signal);
13
24
  }
14
- function readFreeformInput(prompt, values = [], heading = "Suggestions") {
25
+ function readFreeformInput(prompt, values = [], heading = "Suggestions", signal) {
15
26
  const terminal = createInterface({ input: process.stdin, output: process.stdout });
16
27
  const suggestionText = values.length === 0
17
28
  ? ""
18
29
  : `\n${heading}:\n${values.map((value) => `- ${value}`).join("\n")}`;
19
30
  return new Promise((resolve, reject) => {
20
31
  let answered = false;
32
+ function onAbort() {
33
+ answered = true;
34
+ terminal.close();
35
+ // Leave the unanswered prompt line behind.
36
+ process.stdout.write("\n");
37
+ reject(signal?.reason);
38
+ }
21
39
  terminal.once("close", () => {
40
+ signal?.removeEventListener("abort", onAbort);
22
41
  if (!answered)
23
- reject(new Error("Terminal input closed before a response"));
42
+ reject(new TerminalInputClosed());
24
43
  });
44
+ signal?.addEventListener("abort", onAbort, { once: true });
25
45
  terminal.question(`${prompt}${suggestionText}\n> `, (answer) => {
26
46
  answered = true;
27
47
  terminal.close();
@@ -29,7 +49,7 @@ function readFreeformInput(prompt, values = [], heading = "Suggestions") {
29
49
  });
30
50
  });
31
51
  }
32
- function selectHumanInput(prompt, values, allowOther) {
52
+ function selectHumanInput(prompt, values, allowOther, signal) {
33
53
  return new Promise((resolve, reject) => {
34
54
  const choices = allowOther ? [...values, "Other…"] : [...values];
35
55
  const initialRawMode = process.stdin.isRaw;
@@ -54,13 +74,20 @@ function selectHumanInput(prompt, values, allowOther) {
54
74
  process.stdin.off("keypress", onKeypress);
55
75
  process.stdin.off("end", onInputClosed);
56
76
  process.stdin.off("close", onInputClosed);
77
+ signal?.removeEventListener("abort", onAbort);
57
78
  process.stdin.setRawMode(initialRawMode ?? false);
58
79
  if (!initialRawMode)
59
80
  process.stdin.pause();
60
81
  }
61
82
  function onInputClosed() {
62
83
  cleanup();
63
- reject(new Error("Terminal input closed before a response"));
84
+ reject(new TerminalInputClosed());
85
+ }
86
+ function onAbort() {
87
+ clearMenu();
88
+ cleanup();
89
+ process.stdout.write(`${dim("answered elsewhere")}\n`);
90
+ reject(signal?.reason);
64
91
  }
65
92
  function onKeypress(_character, key) {
66
93
  if (key.ctrl && key.name === "c") {
@@ -84,7 +111,7 @@ function selectHumanInput(prompt, values, allowOther) {
84
111
  resolve(answer);
85
112
  }
86
113
  else {
87
- void readFreeformInput("Feedback").then(resolve, reject);
114
+ void readFreeformInput("Feedback", [], undefined, signal).then(resolve, reject);
88
115
  }
89
116
  }
90
117
  }
@@ -94,6 +121,7 @@ function selectHumanInput(prompt, values, allowOther) {
94
121
  process.stdin.on("keypress", onKeypress);
95
122
  process.stdin.once("end", onInputClosed);
96
123
  process.stdin.once("close", onInputClosed);
124
+ signal?.addEventListener("abort", onAbort, { once: true });
97
125
  process.stdin.resume();
98
126
  render();
99
127
  });
@@ -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 VonProviderOptions extends SystemOneHttpOptions {
4
+ /** Overrides VON_BASE_URL, then http://127.0.0.1:8000. */
5
+ readonly baseUrl?: string;
6
+ /** Overrides VON_API_KEY. No TypeSafe/OpenRouter credential fallback. */
7
+ readonly apiKey?: string;
8
+ /** Overrides VON_MODEL, then von-1.0.0. Does NOT select the server backend. */
9
+ readonly model?: string;
10
+ }
11
+ export type VonAgentOptions = VonProviderOptions & DecisionAgentOptions;
12
+ /** One Choice request to a separately managed Von server; no Python dependency. */
13
+ export declare function vonProvider(options?: VonProviderOptions): DecisionProvider;
14
+ /** Convenience binding. Transitions, confidence gates, and retries stay in Machines. */
15
+ export declare function vonAgent(options?: VonAgentOptions): (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
+ /** One Choice request to a separately managed Von server; no Python dependency. */
4
+ export function vonProvider(options = {}) {
5
+ return systemOneHttpProvider({
6
+ name: "von", label: "Von", envPrefix: "VON",
7
+ baseUrl: "http://127.0.0.1:8000", model: "von-1.0.0", minChoices: 1,
8
+ }, options);
9
+ }
10
+ /** Convenience binding. Transitions, confidence gates, and retries stay in Machines. */
11
+ export function vonAgent(options = {}) {
12
+ return decisionAgent(vonProvider(options), options);
13
+ }
@@ -12,7 +12,7 @@ a Human uses a terminal, or a run is displayed in Codex.
12
12
  | State | Named position invoking one Agent, Human, or Operation, or a final state |
13
13
  | Agent | Bounded request implemented by an `AgentRunner` |
14
14
  | Operation | Trusted local function returning an event |
15
- | Human | Input request returning a `submitted` event with the response |
15
+ | Human | Input request returning `submitted`, or an explicitly enabled `question` event |
16
16
  | Run | One execution of a Machine |
17
17
  | Adapter | Mechanism connecting a primitive or run to an external interface |
18
18
 
@@ -37,12 +37,25 @@ MCP server ────┘ ↑
37
37
  | Module | Owns |
38
38
  | --- | --- |
39
39
  | `src/index.ts` | Typed primitives, Machine validation, XState execution, observer contracts |
40
+ | `src/conversation.ts` | Compound Human/clarification state; the owner supplies the original session responder |
40
41
  | `src/terminal-human.ts` | Default terminal Human input, selectors, raw-mode restoration |
41
42
  | `src/machine-module.ts` | Shared module contract and metadata validation |
42
43
  | `src/catalog.ts` | Definition and preset catalog reads |
43
44
  | `src/acp.ts` | ACP process/session protocol, bounded Agent result, normalized activity |
44
45
  | `src/agy.ts` | Antigravity CLI (agy) process protocol, bounded Agent result, normalized activity |
46
+ | `src/chatgpt-web.ts` | ChatGPT web conversation over the DevTools protocol, rig-bridge workspace prompt, bridge activity |
47
+ | `src/claude.ts` | Claude Code CLI headless execution, final-result decoding, permission options, normalized activity |
48
+ | `src/codex.ts` | Codex CLI execution, final-response decoding, sandbox options, normalized activity |
49
+ | `src/deepseek.ts` | DeepSeek Harness command/profile configuration over the shared ACP runner |
50
+ | `src/decision.ts` | Provider-neutral classification contract, validated outcome events, normalized observations |
51
+ | `src/jev.ts` | TypeSafe Choice HTTP transport, authentication, model selection, and bounded requests |
52
+ | `src/openrouter.ts` | OpenRouter Decisions Choice transport, independent credentials, optional result metadata |
53
+ | `src/laya.ts`, `src/von.ts` | Self-hosted Choice providers with independent configuration |
54
+ | `src/system-one-http.ts` | Shared bounded HTTP transport for the self-hosted providers |
55
+ | `services/laya/server.py` | Opt-in, externally managed single-checkpoint Python Choice bridge |
45
56
  | `src/agent-protocol.ts` | Shared Agent prompt instructions and Machines event decoding |
57
+ | `src/agent-limit.ts` | Typed provider usage-limit error and its recognition across module copies |
58
+ | `src/agent-limits.ts` | Limit-aware runner: fallback order, handoff note, reset waiting, per-run memory of limited presets, report-only requests |
46
59
  | `src/discovery.ts` | Project/global definition locations and precedence |
47
60
  | `src/launcher.ts` | Definition metadata, Agent bindings, preflight, single-use launch preparation |
48
61
  | `src/discovery-worker.ts` | Fresh metadata imports in disposable workers |
@@ -68,7 +81,12 @@ Operation rather than the factory.
68
81
 
69
82
  The launcher resolves required semantic Agent roles and configured presets before
70
83
  starting the actor. Missing bindings, invalid overrides, or invalid definitions
71
- fail preflight. The resulting prepared run can be started once. Long-lived hosted
84
+ fail preflight. It then wraps each role's runner so that a usage limit swaps to
85
+ the bound preset's allowed fallbacks or waits for the reset; the runtime still
86
+ receives one runner per role. An Agent state that names an `onLimit` event keeps
87
+ that policy in the Machine: the runtime asks the runner to report the limit and
88
+ delivers it as the event, and the wrapper neither swaps nor waits for that
89
+ request. The resulting prepared run can be started once. Long-lived hosted
72
90
  runs get a fresh child process; direct runtime users own their own definitions.
73
91
 
74
92
  ## Session and process lifecycle
@@ -82,6 +100,16 @@ and that request. Claiming a request before asynchronous delivery prevents two
82
100
  answers from being accepted, and the identity prevents a stale card or dialog from
83
101
  answering a later prompt. Restricted choices are validated for every interface.
84
102
 
103
+ `machine run` asks on its own terminal. With run-status publication on it also
104
+ publishes the pending request and its request ID, and takes the first valid
105
+ response from either the terminal or a private per-run inbox file written by
106
+ `machine respond`; the MCP session takes inbox responses through the same
107
+ session `respond` path as its tool, and inbox questions through the session's
108
+ `ask`. The inbox is a second way to deliver a response to the owning run, not a
109
+ second owner: the run still validates the request ID and choices, and a response
110
+ to any other request is discarded. See
111
+ [Integrations](integrations.md#answering-a-waiting-run-from-outside).
112
+
85
113
  Closing a session terminates active and starting hosts. Host termination includes
86
114
  ordinary descendants in the host's owned process group on POSIX and process-tree
87
115
  termination on Windows. Deliberately detached processes can escape normal ownership;
@@ -95,6 +123,31 @@ Human drafts and focus belong to the presentation adapter while the request ID i
95
123
  unchanged. The card polls authoritative status; it does not decide transitions or
96
124
  persist a second copy of the workflow state.
97
125
 
126
+ ## Discussion-capable Human states
127
+
128
+ Existing Human runners may still return strings. An opt-in request with
129
+ `discussion: true` additionally permits `{ type: "question", text }`; its Human
130
+ state must explicitly handle `question`. Questions never bypass restricted
131
+ choices or become implicit approvals. The public host's `ask(question, requestId)`
132
+ claims the exact current request separately from `respond(answer, requestId)`.
133
+ The child emits the Machine event; the adapter never chooses a destination state.
134
+
135
+ `@dna113p/machines/conversation` exports `conversation(options, on)`, authoring
136
+ sugar for a bounded Human -> owner-supplied clarification -> Human loop. It keeps
137
+ one pending decision, accepts a final explicit answer, and delegates its outcome
138
+ to the surrounding Machine. Its reply callback owns the agent/session reference;
139
+ the helper does not create an assistant or promise that another call to the same
140
+ Agent role resumes the original native session. For arbitrary clarification
141
+ policies, use ordinary `human`, `agent`, and `operation` states instead.
142
+
143
+ Drip uses this public host/runtime boundary to show every Machine run and route
144
+ its Human steps through a shared web/CLI focus. Existing Pi/MCP presentation
145
+ adapters remain answer-only unless they explicitly implement the question channel.
146
+ The default terminal Human selector is also answer-only; Drip's `chat` command
147
+ provides the discussion-capable terminal surface, and `machine respond --question`
148
+ sends a question to a published run, started by `machine run` or hosted by the
149
+ MCP server, whose request allows discussion.
150
+
98
151
  ## Distribution
99
152
 
100
153
  Development runs source TypeScript on Node 24. `npm run build` emits JavaScript and
@@ -111,3 +164,15 @@ the MCP widget resource, and a hosted Human response.
111
164
  The earlier implementation plans are retained in the repository's
112
165
  [history directory](https://github.com/dna113p/machines/tree/main/docs/history) for rationale.
113
166
  Their phase ordering and manual proof gates describe past work.
167
+
168
+ ## External execution owners
169
+
170
+ The supported `@dna113p/machines/host` export exposes `startMachineHost`, its
171
+ observations, correlated Human responses, termination, and final state/output.
172
+ External applications can own hosted runs without importing private modules or
173
+ parsing terminal output. Hosted input and optional output are JSON values.
174
+
175
+ Ticket discovery, persistence, scheduling, and tracker updates belong to the
176
+ separate `machines-dispatch` application. Machines has no dependency on ticketing.
177
+ External owners decide how to interpret output; a final state alone does not
178
+ prove that a ticket should close.