@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.
- package/README.md +115 -11
- package/claude/statusline.mjs +277 -0
- package/dist/examples/chatgpt-web.d.ts +1 -0
- package/dist/examples/chatgpt-web.js +36 -0
- package/dist/examples/claude.d.ts +1 -0
- package/dist/examples/claude.js +30 -0
- package/dist/examples/codex.d.ts +1 -0
- package/dist/examples/codex.js +30 -0
- package/dist/examples/decision.d.ts +1 -0
- package/dist/examples/decision.js +66 -0
- package/dist/examples/deepseek.d.ts +1 -0
- package/dist/examples/deepseek.js +27 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +74 -11
- package/dist/pi-extension/index.js +16 -9
- package/dist/src/acp.js +2 -1
- package/dist/src/agent-bindings.d.ts +6 -0
- package/dist/src/agent-bindings.js +35 -5
- package/dist/src/agent-limit.d.ts +25 -0
- package/dist/src/agent-limit.js +34 -0
- package/dist/src/agent-limits.d.ts +37 -0
- package/dist/src/agent-limits.js +212 -0
- package/dist/src/agent-protocol.js +3 -1
- package/dist/src/catalog.d.ts +3 -0
- package/dist/src/catalog.js +84 -12
- package/dist/src/chatgpt-web.d.ts +143 -0
- package/dist/src/chatgpt-web.js +946 -0
- package/dist/src/claude.d.ts +35 -0
- package/dist/src/claude.js +294 -0
- package/dist/src/cli.js +282 -16
- package/dist/src/codex.d.ts +16 -0
- package/dist/src/codex.js +176 -0
- package/dist/src/conversation.d.ts +74 -0
- package/dist/src/conversation.js +46 -0
- package/dist/src/coordinator-guidance.d.ts +3 -0
- package/dist/src/coordinator-guidance.js +17 -0
- package/dist/src/decision.d.ts +29 -0
- package/dist/src/decision.js +67 -0
- package/dist/src/deepseek.d.ts +12 -0
- package/dist/src/deepseek.js +16 -0
- package/dist/src/host.d.ts +11 -0
- package/dist/src/host.js +153 -16
- package/dist/src/index.d.ts +39 -2
- package/dist/src/index.js +62 -5
- package/dist/src/jev.d.ts +15 -0
- package/dist/src/jev.js +90 -0
- package/dist/src/json.d.ts +6 -0
- package/dist/src/json.js +28 -0
- package/dist/src/launcher.d.ts +11 -1
- package/dist/src/launcher.js +72 -3
- package/dist/src/laya.d.ts +15 -0
- package/dist/src/laya.js +13 -0
- package/dist/src/openrouter.d.ts +16 -0
- package/dist/src/openrouter.js +97 -0
- package/dist/src/run-inbox.d.ts +49 -0
- package/dist/src/run-inbox.js +233 -0
- package/dist/src/run-status.d.ts +121 -0
- package/dist/src/run-status.js +336 -0
- package/dist/src/run-watch.d.ts +24 -0
- package/dist/src/run-watch.js +112 -0
- package/dist/src/session.d.ts +15 -2
- package/dist/src/session.js +47 -8
- package/dist/src/system-one-http.d.ts +25 -0
- package/dist/src/system-one-http.js +146 -0
- package/dist/src/terminal-human.d.ts +8 -1
- package/dist/src/terminal-human.js +36 -8
- package/dist/src/von.d.ts +15 -0
- package/dist/src/von.js +13 -0
- package/docs/architecture.md +74 -3
- package/docs/authoring.md +809 -1
- package/docs/integrations.md +322 -3
- package/docs/local-decisions.md +212 -0
- package/package.json +21 -4
- package/services/laya/server.py +230 -0
- package/skills/machine-builder/SKILL.md +56 -6
- package/skills/machine-delegation/SKILL.md +128 -0
- package/skills/machine-delegation/references/delegation-machine.md +90 -0
- package/skills/machine-project-setup/SKILL.md +181 -0
- package/skills/machine-project-setup/references/project-workflows.md +50 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.12,<3.14"
|
|
3
|
+
# dependencies = ["laya==0.3.4", "fastapi>=0.115,<1", "uvicorn>=0.30,<1"]
|
|
4
|
+
# ///
|
|
5
|
+
"""Opt-in, single-checkpoint Laya Choice bridge. Run with uv run --script server.py.
|
|
6
|
+
|
|
7
|
+
No model is loaded on import. Startup loads a real checkpoint once and fails if
|
|
8
|
+
loading fails. This is a development/self-hosting bridge, not a public gateway.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
13
|
+
import hmac
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
import threading
|
|
18
|
+
from typing import Any, Callable, Literal
|
|
19
|
+
|
|
20
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
21
|
+
from fastapi.concurrency import run_in_threadpool
|
|
22
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
|
23
|
+
|
|
24
|
+
MAX_REQUEST_BYTES = 65_536
|
|
25
|
+
DEFAULT_CHECKPOINT = "convaiinnovations/laya"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ContextLimitError(ValueError):
|
|
29
|
+
"""The SDK would silently truncate evidence, instructions, or options."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ChoiceQuestion(BaseModel):
|
|
33
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
34
|
+
type: Literal["choice"]
|
|
35
|
+
instructions: str = Field(min_length=1)
|
|
36
|
+
criteria: dict[str, str | None] = Field(min_length=2)
|
|
37
|
+
|
|
38
|
+
@field_validator("instructions")
|
|
39
|
+
@classmethod
|
|
40
|
+
def nonblank(cls, value: str) -> str:
|
|
41
|
+
if not value.strip():
|
|
42
|
+
raise ValueError("Empty instructions")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
@field_validator("criteria")
|
|
46
|
+
@classmethod
|
|
47
|
+
def valid_choices(cls, value: dict[str, str | None]) -> dict[str, str | None]:
|
|
48
|
+
if any(not key.strip() or key != key.strip()
|
|
49
|
+
or (description is not None and not description.strip())
|
|
50
|
+
for key, description in value.items()):
|
|
51
|
+
raise ValueError("Invalid choices")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Questions(BaseModel):
|
|
56
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
57
|
+
decision: ChoiceQuestion
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ChoiceRequest(BaseModel):
|
|
61
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
62
|
+
model: str = Field(min_length=1)
|
|
63
|
+
state: str = Field(min_length=1)
|
|
64
|
+
questions: Questions
|
|
65
|
+
|
|
66
|
+
@field_validator("state", "model")
|
|
67
|
+
@classmethod
|
|
68
|
+
def nonblank(cls, value: str) -> str:
|
|
69
|
+
if not value.strip():
|
|
70
|
+
raise ValueError("Empty value")
|
|
71
|
+
return value
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_context(tokenizer: Any, state: str, instructions: str, options: list[str],
|
|
75
|
+
max_len: int, head_max_len: int) -> None:
|
|
76
|
+
"""Reject every truncation path in the pinned Laya 0.3.4 Choice formatter."""
|
|
77
|
+
def count(text: str) -> int:
|
|
78
|
+
return len(tokenizer(text.replace(tokenizer.mask_token, " "),
|
|
79
|
+
add_special_tokens=False)["input_ids"])
|
|
80
|
+
|
|
81
|
+
head = count(f"choice question: {instructions}")
|
|
82
|
+
option_lengths = [count(" " + option) for option in options]
|
|
83
|
+
# The SDK first caps individual option descriptions at 48 tokens, then may
|
|
84
|
+
# shrink all options to reserve 16 tokens of its question/option head budget.
|
|
85
|
+
option_tokens = sum(length + 1 for length in option_lengths) # MASK per option
|
|
86
|
+
budget = head_max_len - option_tokens
|
|
87
|
+
if (any(length > 48 for length in option_lengths) or budget < 16
|
|
88
|
+
or head > max(8, budget)
|
|
89
|
+
or head + option_tokens + count(state) + 4 > max_len):
|
|
90
|
+
raise ContextLimitError("Request exceeds the checkpoint's untruncated context budget")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_predictor(checkpoint: str, subfolder: str | None, device: str | None) -> Any:
|
|
94
|
+
# Optional ML dependencies are deliberately imported only during startup.
|
|
95
|
+
import laya
|
|
96
|
+
from laya.common import render_options
|
|
97
|
+
|
|
98
|
+
agent = laya.load(checkpoint, subfolder=subfolder, device=device)
|
|
99
|
+
|
|
100
|
+
class Predictor:
|
|
101
|
+
def predict(self, state: str, questions: dict[str, Any]) -> Any:
|
|
102
|
+
for question in questions.values():
|
|
103
|
+
internal = {"t": "choice", "ins": question["instructions"], "crit": question["criteria"]}
|
|
104
|
+
check_context(agent.tok, state, question["instructions"], render_options(internal),
|
|
105
|
+
agent.cfg.get("max_len", 512), agent.cfg.get("head_max_len", 192))
|
|
106
|
+
return agent.predict(state, questions)
|
|
107
|
+
|
|
108
|
+
return Predictor()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def normalized_response(raw: Any, model: str, choices: dict[str, Any]) -> dict[str, Any]:
|
|
112
|
+
"""Only validated classification fields leave the bridge, never SDK diagnostics."""
|
|
113
|
+
answer = raw["answers"]["decision"]
|
|
114
|
+
probabilities = answer["probabilities"]
|
|
115
|
+
confidence = answer["confidence"]
|
|
116
|
+
choice = answer["choice"]
|
|
117
|
+
|
|
118
|
+
def probability(value: Any) -> bool:
|
|
119
|
+
return type(value) in (int, float) and math.isfinite(value) and 0 <= value <= 1
|
|
120
|
+
|
|
121
|
+
if (answer["type"] != "choice" or not isinstance(choice, str) or choice not in choices
|
|
122
|
+
or not isinstance(probabilities, dict) or probabilities.keys() != choices.keys()
|
|
123
|
+
or not all(probability(value) for value in probabilities.values())
|
|
124
|
+
or abs(sum(probabilities.values()) - 1) > 0.001
|
|
125
|
+
or probabilities[choice] < max(probabilities.values())
|
|
126
|
+
or not probability(confidence)):
|
|
127
|
+
raise ValueError("Invalid Choice result")
|
|
128
|
+
# The server's configured identifier names its loaded checkpoint; the request
|
|
129
|
+
# cannot select arbitrary Hugging Face repositories or local filesystem paths.
|
|
130
|
+
return {"model": model, "answers": {"decision": {
|
|
131
|
+
"type": "choice", "choice": choice,
|
|
132
|
+
"probabilities": probabilities, "confidence": confidence,
|
|
133
|
+
}}}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def create_app(*, model: str = "laya", checkpoint: str = DEFAULT_CHECKPOINT,
|
|
137
|
+
subfolder: str | None = None, device: str | None = None,
|
|
138
|
+
api_key: str | None = None, loader: Callable[[], Any] | None = None) -> FastAPI:
|
|
139
|
+
if not model.strip() or not checkpoint.strip():
|
|
140
|
+
raise ValueError("Model and checkpoint must be non-empty")
|
|
141
|
+
if api_key is not None and (not api_key or any(not 33 <= ord(char) <= 126 for char in api_key)):
|
|
142
|
+
raise ValueError("LAYA_API_KEY must be a non-empty ASCII Bearer token")
|
|
143
|
+
inference_lock = threading.Lock()
|
|
144
|
+
|
|
145
|
+
@asynccontextmanager
|
|
146
|
+
async def lifespan(app: FastAPI):
|
|
147
|
+
app.state.predictor = await run_in_threadpool(
|
|
148
|
+
loader if loader is not None else lambda: load_predictor(checkpoint, subfolder, device)
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
yield
|
|
152
|
+
finally:
|
|
153
|
+
del app.state.predictor
|
|
154
|
+
|
|
155
|
+
app = FastAPI(title="Machines Laya Choice bridge", lifespan=lifespan,
|
|
156
|
+
docs_url=None, redoc_url=None, openapi_url=None)
|
|
157
|
+
|
|
158
|
+
def authorize(request: Request) -> None:
|
|
159
|
+
if api_key is not None and not hmac.compare_digest(
|
|
160
|
+
request.headers.get("authorization", "").encode(), f"Bearer {api_key}".encode()
|
|
161
|
+
):
|
|
162
|
+
raise HTTPException(401, "Unauthorized")
|
|
163
|
+
|
|
164
|
+
@app.get("/health")
|
|
165
|
+
def health(request: Request) -> dict[str, Any]:
|
|
166
|
+
authorize(request)
|
|
167
|
+
if not hasattr(app.state, "predictor"):
|
|
168
|
+
raise HTTPException(503, "Checkpoint not loaded")
|
|
169
|
+
return {"status": "ready", "service": "machines-laya", "model": model}
|
|
170
|
+
|
|
171
|
+
def evaluate(payload: ChoiceRequest) -> dict[str, Any]:
|
|
172
|
+
# Reject concurrent work instead of creating an unbounded inference queue.
|
|
173
|
+
if not inference_lock.acquire(blocking=False):
|
|
174
|
+
raise HTTPException(503, "Laya is busy; no automatic retry")
|
|
175
|
+
try:
|
|
176
|
+
try:
|
|
177
|
+
raw = app.state.predictor.predict(payload.state, payload.questions.model_dump())
|
|
178
|
+
except ContextLimitError:
|
|
179
|
+
raise HTTPException(413, "Request exceeds the checkpoint's untruncated context budget") from None
|
|
180
|
+
except Exception:
|
|
181
|
+
raise HTTPException(500, "Laya inference failed") from None
|
|
182
|
+
try:
|
|
183
|
+
return normalized_response(raw, model, payload.questions.decision.criteria)
|
|
184
|
+
except Exception:
|
|
185
|
+
raise HTTPException(502, "Laya returned an invalid Choice response") from None
|
|
186
|
+
finally:
|
|
187
|
+
inference_lock.release()
|
|
188
|
+
|
|
189
|
+
@app.post("/v1/systemone")
|
|
190
|
+
async def system_one(request: Request) -> dict[str, Any]:
|
|
191
|
+
authorize(request)
|
|
192
|
+
body = bytearray()
|
|
193
|
+
async for chunk in request.stream():
|
|
194
|
+
body.extend(chunk)
|
|
195
|
+
if len(body) > MAX_REQUEST_BYTES:
|
|
196
|
+
raise HTTPException(413, "Request exceeds 64 KiB")
|
|
197
|
+
try:
|
|
198
|
+
payload = ChoiceRequest.model_validate(json.loads(body))
|
|
199
|
+
except (ValueError, ValidationError, UnicodeError):
|
|
200
|
+
# Default validation errors can echo the original evidence.
|
|
201
|
+
raise HTTPException(422, "Expected one Choice question named decision and string state") from None
|
|
202
|
+
if payload.model != model:
|
|
203
|
+
raise HTTPException(404, "Unknown served model")
|
|
204
|
+
return await run_in_threadpool(evaluate, payload)
|
|
205
|
+
|
|
206
|
+
return app
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def main() -> None:
|
|
210
|
+
import uvicorn
|
|
211
|
+
|
|
212
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
213
|
+
parser.add_argument("--host", default="127.0.0.1")
|
|
214
|
+
parser.add_argument("--port", type=int, default=8001)
|
|
215
|
+
parser.add_argument("--model", default="laya", help="Public identifier expected by LAYA_MODEL")
|
|
216
|
+
parser.add_argument("--checkpoint", default=DEFAULT_CHECKPOINT)
|
|
217
|
+
parser.add_argument("--subfolder", default=None)
|
|
218
|
+
parser.add_argument("--device", default=None, help="For example cpu, cuda, or mps")
|
|
219
|
+
args = parser.parse_args()
|
|
220
|
+
api_key = os.environ.get("LAYA_API_KEY")
|
|
221
|
+
if args.host not in ("127.0.0.1", "::1", "localhost") and not api_key:
|
|
222
|
+
parser.error("Non-loopback binding requires LAYA_API_KEY; use a TLS proxy on untrusted networks")
|
|
223
|
+
app = create_app(model=args.model, checkpoint=args.checkpoint, subfolder=args.subfolder,
|
|
224
|
+
device=args.device, api_key=api_key)
|
|
225
|
+
# One process, one resident model. No auto-reload or request/evidence logging.
|
|
226
|
+
uvicorn.run(app, host=args.host, port=args.port, access_log=False)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
main()
|
|
@@ -1,24 +1,74 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: machine-builder
|
|
3
|
-
description:
|
|
3
|
+
description: Use proactively for multi-step implementation, debugging, review, or verification that benefits from explicit stages and decisions. Discover and use a suitable Machine, or design and verify a task-specific TypeScript workflow. The user need not explicitly request Machines; keep trivial work direct.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Machine builder
|
|
7
7
|
|
|
8
|
+
For project-wide workflow setup owned by the project orchestrator, start with
|
|
9
|
+
[machine-project-setup](../machine-project-setup/SKILL.md). It chooses coverage
|
|
10
|
+
for the project's major work; use this builder to implement individual definitions.
|
|
11
|
+
Do not repeat project setup for each assignment.
|
|
12
|
+
|
|
13
|
+
Before starting multi-step work, discover suitable Machines instead of waiting for
|
|
14
|
+
the user to ask for one. Use an existing workflow when it fits; writing a new
|
|
15
|
+
Machine is not the goal. For delegated work, follow
|
|
16
|
+
[machine-delegation](../machine-delegation/SKILL.md). If already executing an Agent
|
|
17
|
+
state inside a Machine, do that assigned work directly rather than recursively
|
|
18
|
+
wrapping it in another Machine. Preserve the authorized effects and model budget.
|
|
19
|
+
|
|
20
|
+
Decide which workflow serves the user's task. Creating or adapting a project
|
|
21
|
+
Machine is an implementation choice within the authorized task; proceed without
|
|
22
|
+
separate permission to author it. Choose its states, roles, branches, and completion
|
|
23
|
+
conditions from the work itself. A Machine can be useful for one assignment;
|
|
24
|
+
future reuse is optional. Keep small steps direct when a workflow adds no value.
|
|
25
|
+
|
|
8
26
|
Use [Authoring Machines](../../docs/authoring.md) for the definition contract and
|
|
9
27
|
examples. Before choosing a workflow, inspect available Machines and Agent presets
|
|
10
28
|
with `machine list` and `machine agents`, or the connected `machine_list` and
|
|
11
|
-
`machine_agents` tools.
|
|
29
|
+
`machine_agents` tools. Reuse, adapt, or create a definition based on the task's
|
|
30
|
+
needs. Existing Machines and examples are starting points, not a fixed menu.
|
|
31
|
+
Make consequential checks and decisions explicit in states when they need to
|
|
32
|
+
control progress. Choose the complexity needed to complete the task reliably.
|
|
33
|
+
|
|
34
|
+
Look for common task sequences and recurring manual workflows while doing the
|
|
35
|
+
work. Convert them into Machines when explicit steps would improve repeatability
|
|
36
|
+
or code quality. Parameterize assignment details so the workflow can serve the
|
|
37
|
+
next task. Build around the process observed in the project.
|
|
38
|
+
|
|
39
|
+
Use deterministic Operations for mechanically checkable work: project type checks,
|
|
40
|
+
focused tests, linting, schema or artifact validation, and filesystem or Git
|
|
41
|
+
invariants. Inspect actual exit codes and outputs, record useful evidence, and
|
|
42
|
+
route failures to repair, escalation, or a blocked result. Required checks must
|
|
43
|
+
pass before success; an Agent's claim of completion is not verification. Use
|
|
44
|
+
Agents for judgment and open-ended changes, with bounded repair loops where
|
|
45
|
+
appropriate. Choose checks that establish the task's completion conditions.
|
|
12
46
|
|
|
13
47
|
For a new definition, capture the intended states, returned events, and completion
|
|
14
48
|
condition. Put the definition in the requested location; project `.machines/` is
|
|
15
|
-
the default for project-specific work.
|
|
49
|
+
the default for project-specific work. Authoring a Machine does not expand the
|
|
50
|
+
task's authorized effects; preserve that scope when running it. Global installation
|
|
51
|
+
is a separate choice.
|
|
16
52
|
|
|
17
53
|
Export a non-empty one-line `description` explaining when to use the Machine and
|
|
18
|
-
a default factory accepting the supplied primitives and input
|
|
19
|
-
|
|
54
|
+
a default factory accepting the supplied primitives and input. Import runtime
|
|
55
|
+
types from `@dna113p/machines`, for example:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import type { Event, MachinePrimitives } from "@dna113p/machines";
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use public package exports for runtime imports and relative imports for
|
|
62
|
+
project-local helpers. A sibling checkout's `src/` is not the package interface.
|
|
63
|
+
Install the package locally when editor or type-checker resolution is needed;
|
|
64
|
+
the launcher supplies primitives and type-only imports are erased at execution.
|
|
65
|
+
|
|
66
|
+
Declare semantic `agentRoles` for named Agents, then use those exact roles in states.
|
|
20
67
|
Select harnesses and models through presets, keeping environment details out of
|
|
21
|
-
workflow policy.
|
|
68
|
+
workflow policy. A provider usage limit normally swaps to a fallback preset or
|
|
69
|
+
waits; give an Agent state `onLimit: "<event>"` only when the workflow itself
|
|
70
|
+
should branch at a limit, for example to skip a cross-provider review or ask a
|
|
71
|
+
Human. The [bundled examples](../../dist/examples/) show runtime usage
|
|
22
72
|
after the package is built; authoring examples in the guide also work before a build.
|
|
23
73
|
|
|
24
74
|
Keep module initialization side-effect free: discovery imports definitions and
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: machine-delegation
|
|
3
|
+
description: Use proactively before delegating implementation, debugging, review, research, or other bounded multi-step work. Prefer Machine runs to native sub-agents, reusing or creating a scoped workflow with explicit outcomes. The user need not name Machines; keep trivial work direct.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Machine delegation
|
|
7
|
+
|
|
8
|
+
When acting as project orchestrator, consult the project's `.machines/README.md`
|
|
9
|
+
or existing workflow map before choosing a route. On initial setup or meaningful
|
|
10
|
+
missing/stale coverage, use [machine-project-setup](../machine-project-setup/SKILL.md)
|
|
11
|
+
to build the useful project workflows before repeated delegation. Reuse established
|
|
12
|
+
coverage for normal tasks; workers do not repeat project setup.
|
|
13
|
+
|
|
14
|
+
Use a Machine run as the unit of delegated work. The coordinating agent owns the
|
|
15
|
+
user's goal, task boundaries, and integration of results; the Machine owns its
|
|
16
|
+
states, verification, retries, and escalation. Complete small local steps directly.
|
|
17
|
+
Launch a suitable Machine instead of native sub-agents without requiring the user
|
|
18
|
+
to name this skill. Discovery is a decision step, not the completed work: start the
|
|
19
|
+
selected workflow and inspect its result. Use deterministic Operations for checks
|
|
20
|
+
rather than paying another model to run commands. Never broaden permissions or
|
|
21
|
+
model spending merely to satisfy the preference. If the tools or a safe workflow
|
|
22
|
+
are unavailable, state the concrete reason for proceeding directly.
|
|
23
|
+
|
|
24
|
+
If already running as a Machine's Agent state, execute that bounded assignment
|
|
25
|
+
and return its evidence/outcome. Do not delegate it again unless explicitly asked
|
|
26
|
+
for bounded nested work by the owning assignment.
|
|
27
|
+
|
|
28
|
+
## Choose or create the Machine
|
|
29
|
+
|
|
30
|
+
Inspect `machine_list` and `machine_agents` in the target workspace. MCP requires
|
|
31
|
+
an absolute `cwd`; Pi uses the conversation's working directory. With only a shell,
|
|
32
|
+
run `machine list` and `machine agents` there (`./machine` in a source checkout).
|
|
33
|
+
See [Integrations](../../docs/integrations.md) if the tools are unavailable.
|
|
34
|
+
|
|
35
|
+
Evaluate candidate Machines by their descriptions, then read their returned source
|
|
36
|
+
paths or use `machine show <name>`. Check input format, Agent roles, actual workspace,
|
|
37
|
+
output artifacts, final states, and effects such as commits or publishing. A matching
|
|
38
|
+
name alone does not establish that the workflow fits the delegated task.
|
|
39
|
+
|
|
40
|
+
Decide whether to reuse, adapt, or create a Machine using
|
|
41
|
+
[machine-builder](../machine-builder/SKILL.md). A generic Machine's ability to
|
|
42
|
+
accept the task as a prompt is only one consideration; choose a workflow whose
|
|
43
|
+
states express the decisions and verification the task needs. Authoring a
|
|
44
|
+
task-specific Machine is part of the delegated work and needs no separate
|
|
45
|
+
permission. Keep global installation and unrelated workflow changes outside that
|
|
46
|
+
scope. Use the [delegation starter](references/delegation-machine.md) when there is no existing
|
|
47
|
+
result contract to follow. Add review or bounded revision states when the work
|
|
48
|
+
needs them, and Human states for decisions that belong to the user.
|
|
49
|
+
|
|
50
|
+
List again after edits. Resolve required roles using exact preset names from
|
|
51
|
+
`machine_agents`, passing role-to-preset overrides at launch. A listing's readiness
|
|
52
|
+
only checks metadata and bindings; the harness still needs to be installed and
|
|
53
|
+
configured. The built-in `codex` preset is read-only. Choose or configure a preset
|
|
54
|
+
that can perform the authorized task, following the builder's authoring guide.
|
|
55
|
+
|
|
56
|
+
## Supply a complete assignment
|
|
57
|
+
|
|
58
|
+
Workers do not inherit this conversation. Supply the context needed to act:
|
|
59
|
+
|
|
60
|
+
- The bounded objective, relevant facts, and acceptance criteria.
|
|
61
|
+
- The absolute workspace, files or references to read, and ownership of any edits.
|
|
62
|
+
- Applicable constraints, permitted effects, and what should produce a blocked outcome.
|
|
63
|
+
- A result location readable by the coordinator, and what evidence it must contain.
|
|
64
|
+
|
|
65
|
+
Follow the selected Machine's input contract. MCP/Pi accept JSON-serializable
|
|
66
|
+
input; CLI positional input is text, while `--input-file <file|->` accepts JSON.
|
|
67
|
+
Keep task details in input rather than hardcoding each assignment into a new
|
|
68
|
+
workflow. Give each run a distinct artifact destination.
|
|
69
|
+
|
|
70
|
+
**Plan result retrieval before launch.** `machine_status` exposes state, Human
|
|
71
|
+
requests, errors, and optional JSON Machine output, not arbitrary Agent transcripts
|
|
72
|
+
or event payloads. Have the Machine return useful JSON output, have the worker
|
|
73
|
+
write a report, or capture its returned event and save it in an Operation. The
|
|
74
|
+
Machine-owned options also support read-only Agent presets. Include findings or
|
|
75
|
+
changes, verification evidence, and blockers. An Operation's writes must stay
|
|
76
|
+
within the task's scope even when the Agent itself is read-only.
|
|
77
|
+
|
|
78
|
+
Delegate independent tasks concurrently only when their workspaces and writes are
|
|
79
|
+
compatible. Use isolated worktrees for overlapping code changes; runs do not create
|
|
80
|
+
worktrees automatically. Pass prerequisites through explicit artifacts before
|
|
81
|
+
starting dependent work. Give workers bounded assignments to complete themselves;
|
|
82
|
+
nested delegation needs an explicit reason and scope.
|
|
83
|
+
|
|
84
|
+
## Launch and supervise
|
|
85
|
+
|
|
86
|
+
Call `machine_start` with the discovered Machine name, task input, workspace, and
|
|
87
|
+
any preset overrides. Record the returned `run.id` with its task, workspace,
|
|
88
|
+
expected success state, and result path. Start once; a slow response is not a
|
|
89
|
+
reason to launch a duplicate. If the launch response is lost, inspect this session's
|
|
90
|
+
statuses before deciding whether another run is needed.
|
|
91
|
+
|
|
92
|
+
Continue independent work while the run executes. Read `machine_status` with its
|
|
93
|
+
exact `runId` when progress or a result is needed, spacing checks rather than
|
|
94
|
+
polling continuously. Interpret the current snapshot:
|
|
95
|
+
|
|
96
|
+
| Status | Coordinator action |
|
|
97
|
+
| --- | --- |
|
|
98
|
+
| `running` | Continue independent work or wait before checking again. |
|
|
99
|
+
| `waiting` | Read `human.prompt` and its choices or suggestions. Relay the question if the user's answer is not already clear from the conversation. Submit that answer with `machine_respond`, using the exact `runId` and current `human.requestId`; restrictive choices require an exact string. |
|
|
100
|
+
| `completed` | Check the final state and read the result artifacts. A final state such as `blocked` or `rejected` also counts as a completed run. |
|
|
101
|
+
| `failed` | Read the error and inspect partial effects. Correct the cause or report the blocker before considering a fresh run. |
|
|
102
|
+
|
|
103
|
+
Preserve existing user authorization when answering Human requests, and surface a
|
|
104
|
+
new decision when the request requires one. A desire to finish is not an answer.
|
|
105
|
+
|
|
106
|
+
To follow a run this session did not start, such as a ticket a daemon runs, use
|
|
107
|
+
`machine watch <ticket-or-run-id>` where run status is published (`machine runs`
|
|
108
|
+
lists what can be watched; `--all` adds runs that already ended). Watching only
|
|
109
|
+
shows the run on this session's status line: it neither owns nor answers it.
|
|
110
|
+
|
|
111
|
+
The CLI fallback is `machine run <name> [--agent role=preset] -- <input>` in the
|
|
112
|
+
target workspace. It waits for completion and uses terminal Human input; retain
|
|
113
|
+
the shell session if running it asynchronously. It does not return an MCP run ID.
|
|
114
|
+
|
|
115
|
+
The current tools have no per-run cancel, arbitrary message, or resume operation.
|
|
116
|
+
`machine_respond` answers a pending Human request; it cannot steer a running Agent.
|
|
117
|
+
Edits to a definition apply to future runs. MCP/Pi runs belong to their session:
|
|
118
|
+
closing or reloading it terminates owned work, and only a limited number of finished
|
|
119
|
+
snapshots are retained. Keep needed evidence in artifacts. After interruption,
|
|
120
|
+
inspect existing effects before restarting; a new run is not a continuation or rollback.
|
|
121
|
+
|
|
122
|
+
## Collect and integrate
|
|
123
|
+
|
|
124
|
+
Finish only after the intended final state and requested artifact or effect are
|
|
125
|
+
verified. Read the report and inspect relevant changes or checks, then integrate
|
|
126
|
+
the result into the user's task. If evidence is missing, treat that as incomplete
|
|
127
|
+
work even if the run says `completed`. Report the useful result, any blocker, and
|
|
128
|
+
artifact paths; a run ID alone is not the delegated deliverable.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# A Machine for one delegated assignment
|
|
2
|
+
|
|
3
|
+
Use this starter when no existing Machine provides the needed task and result
|
|
4
|
+
contract. Save it as `.machines/delegate-task.ts`, then adapt verification to the
|
|
5
|
+
assignment using [Authoring Machines](../../../docs/authoring.md). It accepts a JSON
|
|
6
|
+
input string containing `task` and an absolute `resultPath` that does not yet exist.
|
|
7
|
+
The Agent works in the launch workspace and returns a summary; an Operation saves
|
|
8
|
+
the result, so the Agent can use a read-only preset for research or review.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname, isAbsolute } from "node:path";
|
|
13
|
+
import type { Event, MachinePrimitives } from "@dna113p/machines";
|
|
14
|
+
|
|
15
|
+
export const description = "Completes a bounded assignment and saves its outcome and evidence as JSON.";
|
|
16
|
+
export const agentRoles = { worker: "Performs the delegated assignment" };
|
|
17
|
+
|
|
18
|
+
export default function delegateTask(
|
|
19
|
+
{ machine, agent, operation, final }: MachinePrimitives,
|
|
20
|
+
input: string,
|
|
21
|
+
) {
|
|
22
|
+
const assignment = JSON.parse(input);
|
|
23
|
+
if (
|
|
24
|
+
typeof assignment?.task !== "string" || assignment.task.trim() === "" ||
|
|
25
|
+
typeof assignment?.resultPath !== "string" || !isAbsolute(assignment.resultPath)
|
|
26
|
+
) {
|
|
27
|
+
throw new Error("Expected task and absolute resultPath in JSON input");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let result: { outcome: string; summary: string } | undefined;
|
|
31
|
+
const capture = ({ event }: { event: Event }) => {
|
|
32
|
+
if (typeof event.summary !== "string" || event.summary.trim() === "") {
|
|
33
|
+
throw new Error("Worker must return a non-empty summary with evidence or blockers");
|
|
34
|
+
}
|
|
35
|
+
result = { outcome: event.type, summary: event.summary };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return machine({
|
|
39
|
+
initial: "work",
|
|
40
|
+
states: {
|
|
41
|
+
work: agent(
|
|
42
|
+
`${assignment.task}\n\nComplete this bounded assignment yourself. ` +
|
|
43
|
+
"Return completed when its acceptance criteria are met, or blocked when " +
|
|
44
|
+
"you cannot proceed within scope. Include a summary string in the final " +
|
|
45
|
+
"Machines event with findings or changes, verification evidence, and blockers. " +
|
|
46
|
+
"The Machine saves that summary; you do not need to write a separate report.",
|
|
47
|
+
{
|
|
48
|
+
completed: { target: "save", actions: capture },
|
|
49
|
+
blocked: { target: "save", actions: capture },
|
|
50
|
+
},
|
|
51
|
+
{ using: "worker", cwd: process.cwd() },
|
|
52
|
+
),
|
|
53
|
+
save: operation(async () => {
|
|
54
|
+
if (result === undefined) throw new Error("Worker result is missing");
|
|
55
|
+
await mkdir(dirname(assignment.resultPath), { recursive: true });
|
|
56
|
+
await writeFile(assignment.resultPath, JSON.stringify(result, null, 2) + "\n", {
|
|
57
|
+
flag: "wx",
|
|
58
|
+
});
|
|
59
|
+
return { type: result.outcome };
|
|
60
|
+
}, { completed: "done", blocked: "blocked" }),
|
|
61
|
+
done: final(),
|
|
62
|
+
blocked: final(),
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Discover the definition and presets, then bind `worker` to a discovered preset.
|
|
69
|
+
For example, if `codex` is available and configured, launch a read-only review via
|
|
70
|
+
MCP with these arguments (replace the example paths and task):
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"cwd": "/absolute/project",
|
|
75
|
+
"machine": "delegate-task",
|
|
76
|
+
"input": "{\"task\":\"Read src/parser.ts and its tests. Identify unhandled input cases with file and line evidence. Leave source files unchanged.\",\"resultPath\":\"/absolute/project/.machines/results/parser-review-01.json\"}",
|
|
77
|
+
"agents": { "worker": "codex" }
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The saved JSON has `outcome` and `summary`. Read it after the run finishes; `done`
|
|
82
|
+
means the worker reported completion and its summary was saved. `blocked` is a
|
|
83
|
+
terminal outcome requiring the coordinator's attention. This starter validates
|
|
84
|
+
the result contract, not the truth of the worker's claims: inspect its evidence,
|
|
85
|
+
and add verification Operations for conditions the Machine can check directly.
|
|
86
|
+
|
|
87
|
+
Before real work, exercise both outcomes with fake runners through the public
|
|
88
|
+
runtime or launcher. Also check that a missing summary fails and that an existing
|
|
89
|
+
result file is preserved. Use a fresh result path for each run; retrying after a
|
|
90
|
+
write failure may repeat completed Agent work, so inspect partial effects first.
|