@hanzo/build 0.2.30 → 0.2.31

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.
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Starting a coding run.
2
+ * Starting a coding run, and what it left.
3
3
  *
4
- * POST /v1/agent/coding → 202 {sessionId, repo, branch, project, routed, targetId}
4
+ * POST /v1/agent/coding → 202 {sessionId, repo, branch, routed, targetId}
5
+ * GET /v1/agent/coding/{session}/artifacts {session, saved, artifacts}
6
+ * GET /v1/agent/coding/{session}/artifacts/{name} one stored artifact's bytes
5
7
  *
6
8
  * 202 is ADMITTED, not finished: the answer is the session the run narrates
7
9
  * itself in, and the session stream is how it is watched.
@@ -22,6 +24,9 @@
22
24
  * `setup` is a plan's shape put to one question: how this codebase is installed,
23
25
  * started and checked. Its agent may install and run things in its sandbox, and
24
26
  * its answer is kept as the codebase's proposed environment (environment.ts).
27
+ *
28
+ * A sandbox run keeps its sandbox after it ends, so `after` continues the earlier
29
+ * run in that same sandbox and working tree, resumed if it parked.
25
30
  */
26
31
  import { type Target } from './call.ts';
27
32
  export type Mode = 'build' | 'plan' | 'setup';
@@ -52,7 +57,6 @@ export interface Run {
52
57
  session: string;
53
58
  repo: string;
54
59
  branch: string;
55
- project: string;
56
60
  routed: boolean;
57
61
  target: string;
58
62
  }
@@ -99,3 +103,25 @@ export declare function approve(e: Earlier, plan: string): Ask;
99
103
  * with none recorded, its title's.
100
104
  */
101
105
  export declare function retry(e: Earlier, ask: string): Ask;
106
+ /** What a run left: a changed file, the whole change as a patch, or a link. */
107
+ export type Kind = 'file' | 'patch' | 'preview' | 'pull' | 'deploy';
108
+ export interface Artifact {
109
+ /** A changed file's path in the workspace, `changes.patch`, or a link's name. */
110
+ name: string;
111
+ kind: Kind;
112
+ /** A stored artifact's length in bytes; 0 for a link. */
113
+ size: number;
114
+ /** Where a link points: a preview's host, a pull request, a deployment. */
115
+ url: string;
116
+ /** A preview's port in the run's sandbox. */
117
+ port: number;
118
+ }
119
+ export interface Left {
120
+ /** When the artifacts were saved, or '' while the run has saved none. */
121
+ saved: string;
122
+ artifacts: Artifact[];
123
+ }
124
+ /** What a run left, kept after its sandbox is gone. */
125
+ export declare function artifacts(t: Target, session: string): Promise<Left>;
126
+ /** One stored artifact's bytes. */
127
+ export declare function artifact(t: Target, session: string, name: string): Promise<Blob>;
package/lib/api/coding.js CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Starting a coding run.
2
+ * Starting a coding run, and what it left.
3
3
  *
4
- * POST /v1/agent/coding → 202 {sessionId, repo, branch, project, routed, targetId}
4
+ * POST /v1/agent/coding → 202 {sessionId, repo, branch, routed, targetId}
5
+ * GET /v1/agent/coding/{session}/artifacts {session, saved, artifacts}
6
+ * GET /v1/agent/coding/{session}/artifacts/{name} one stored artifact's bytes
5
7
  *
6
8
  * 202 is ADMITTED, not finished: the answer is the session the run narrates
7
9
  * itself in, and the session stream is how it is watched.
@@ -22,8 +24,11 @@
22
24
  * `setup` is a plan's shape put to one question: how this codebase is installed,
23
25
  * started and checked. Its agent may install and run things in its sandbox, and
24
26
  * its answer is kept as the codebase's proposed environment (environment.ts).
27
+ *
28
+ * A sandbox run keeps its sandbox after it ends, so `after` continues the earlier
29
+ * run in that same sandbox and working tree, resumed if it parked.
25
30
  */
26
- import { call, Refusal } from './call.js';
31
+ import { call, headers, reason, Refusal, seg } from './call.js';
27
32
  /** Why this mode cannot run where it is sent, or '' when it can. */
28
33
  export const unhonoured = (mode, target) => (mode === 'plan' || mode === 'setup') && target?.trim()
29
34
  ? `A ${mode} runs in the Hanzo sandbox: a machine clones and pushes with its own credential. Choose Cloud, or switch to Build.`
@@ -58,7 +63,6 @@ export async function start(t, ask) {
58
63
  session: s('sessionId'),
59
64
  repo: s('repo'),
60
65
  branch: s('branch'),
61
- project: s('project'),
62
66
  routed: r?.routed === true,
63
67
  target: s('targetId'),
64
68
  };
@@ -77,11 +81,11 @@ export function headline(title, repo) {
77
81
  return t;
78
82
  }
79
83
  /**
80
- * The same codebase, place and project as the earlier run, starting from its
81
- * branch when it pushed one. `after` IS that branch as the base (BaseOf,
82
- * apps/coding coding.go), so it is named only then: a plan, a setup, or a build
83
- * that changed nothing pushed no branch to clone, and its follow-up starts where
84
- * it did.
84
+ * The same codebase, place and project as the earlier run. In the sandbox it goes
85
+ * on in the earlier run's own sandbox and working tree (`after`), whatever that
86
+ * run pushed. A machine keeps no sandbox: there `after` is the branch the earlier
87
+ * run pushed, named only when it pushed one, and otherwise the follow-up starts
88
+ * where it did.
85
89
  */
86
90
  function from(e) {
87
91
  const place = e.environment && e.environment !== 'sandbox' ? e.environment : undefined;
@@ -89,7 +93,7 @@ function from(e) {
89
93
  repo: e.repo || undefined,
90
94
  project: e.project || undefined,
91
95
  targetId: place,
92
- ...(e.pushed ? { after: e.id } : { base: e.base || undefined }),
96
+ ...(!place || e.pushed ? { after: e.id } : { base: e.base || undefined }),
93
97
  };
94
98
  }
95
99
  const modeOf = (m) => (m === 'plan' || m === 'setup' ? m : 'build');
@@ -127,3 +131,31 @@ export function approve(e, plan) {
127
131
  export function retry(e, ask) {
128
132
  return { ...from(e), prompt: ask.trim() || headline(e.title, e.repo), mode: modeOf(e.mode) };
129
133
  }
134
+ const KINDS = ['file', 'patch', 'preview', 'pull', 'deploy'];
135
+ /** What a run left, kept after its sandbox is gone. */
136
+ export async function artifacts(t, session) {
137
+ const r = (await call(t, 'GET', `/v1/agent/coding/${seg(session)}/artifacts`)) ?? {};
138
+ const list = Array.isArray(r.artifacts) ? r.artifacts : [];
139
+ return {
140
+ saved: typeof r.saved === 'string' ? r.saved : '',
141
+ artifacts: list.flatMap((raw) => {
142
+ const a = raw && typeof raw === 'object' ? raw : {};
143
+ const kind = KINDS.find((k) => k === a.kind);
144
+ const name = typeof a.name === 'string' ? a.name : '';
145
+ if (!kind || !name)
146
+ return [];
147
+ return [{ name, kind, size: typeof a.size === 'number' ? a.size : 0, url: typeof a.url === 'string' ? a.url : '', port: typeof a.port === 'number' ? a.port : 0 }];
148
+ }),
149
+ };
150
+ }
151
+ /** One stored artifact's bytes. */
152
+ export async function artifact(t, session, name) {
153
+ const parts = name.split('/');
154
+ if (parts.some((p) => p === '' || p === '.' || p === '..'))
155
+ throw new Refusal(400, 'That artifact has no name to fetch it by');
156
+ const res = await fetch(`${t.api}/v1/agent/coding/${seg(session)}/artifacts/${parts.map(seg).join('/')}`, { headers: headers(t), cache: 'no-store' });
157
+ if (!res.ok)
158
+ throw new Refusal(res.status, (await reason(res)) || `The artifact answered ${res.status}`);
159
+ // Bytes, whatever type the store named: a page is saved, never opened here.
160
+ return new Blob([await res.arrayBuffer()], { type: 'application/octet-stream' });
161
+ }
@@ -1,20 +1,53 @@
1
1
  /**
2
- * A run's sandbox while the run holds it: a door into its screen or its shell,
3
- * and its working tree as it is right now.
2
+ * A run's sandbox. It outlives the run: kept after the run ends, parked (its pod
3
+ * stopped, its disk and working tree kept) while nobody watches, and retired a
4
+ * day after it parks; a free plan's sandbox is not parked, and ends within the
5
+ * hour. Resume gives a parked one a pod again under the same id and tree.
4
6
  *
5
- * POST /v1/sandbox/{id}/{screen|terminal}/ticket one ticket, spent when the page's socket opens (30 s)
6
- * POST /v1/sandbox/read {id, path} a directory's names, or a file's bytes
7
+ * GET /v1/sandbox/{id} its status and when its lease ends
8
+ * POST /v1/sandbox/{id}/pause park it: stop the pod, keep the disk
9
+ * POST /v1/sandbox/{id}/resume a pod again, same id and /work
10
+ * POST /v1/sandbox/{id}/{screen|terminal}/ticket one ticket, spent when the page's socket opens (30 s)
11
+ * GET /v1/sandbox/{id}/ports what listens in it, each with its preview host
12
+ * POST /v1/sandbox/{id}/preview {port} an address that opens that port in a browser
13
+ * POST /v1/sandbox/read {id, path} a directory's names, or a file's bytes
7
14
  *
8
- * The door is cloud's own page — noVNC for the screen, xterm for the shell — so
9
- * the builder frames it and never speaks the socket itself. The sandbox is gone
10
- * when the run ends, and every call here then answers 404.
15
+ * A door or a preview asked of a sandbox that stopped as it was asked is 409: it
16
+ * is resumed, and asked again once.
11
17
  */
12
18
  import { type Target } from './call.ts';
13
19
  export type Door = 'screen' | 'terminal';
20
+ /** Where a sandbox is in its life. */
21
+ export type State = 'pending' | 'running' | 'parked' | 'error' | 'gone';
22
+ export interface Box {
23
+ id: string;
24
+ state: State;
25
+ /** When its lease ends, or a parked sandbox is retired; 0 when unknown. Unix ms. */
26
+ ends: number;
27
+ /** Why it could not come up, with state `error`. */
28
+ error: string;
29
+ }
30
+ /** The sandbox as it stands. One the caller no longer holds is gone. */
31
+ export declare function sandbox(t: Target, id: string): Promise<Box>;
32
+ /** Park it: its pod stops, its disk and working tree stay. */
33
+ export declare function park(t: Target, id: string): Promise<Box>;
34
+ /** Give it a pod again, under the same id and tree. */
35
+ export declare function wake(t: Target, id: string): Promise<Box>;
14
36
  /** A shell's tmux session name: the sandbox reattaches to it when the door opens again. */
15
37
  export declare const NAME: RegExp;
16
38
  /** Where to frame one door, with a fresh ticket in its address. */
17
- export declare function door(t: Target, sandbox: string, which: Door, session?: string): Promise<string>;
39
+ export declare function door(t: Target, id: string, which: Door, session?: string): Promise<string>;
40
+ export interface Port {
41
+ port: number;
42
+ host: string;
43
+ }
44
+ /** What listens in the sandbox, ascending. */
45
+ export declare function ports(t: Target, id: string): Promise<Port[]>;
46
+ /**
47
+ * An address that opens one port in a browser: its own origin, with a single-use
48
+ * ticket that sets the preview's cookie and lands on its root.
49
+ */
50
+ export declare function preview(t: Target, id: string, port: number): Promise<string>;
18
51
  export interface Node {
19
52
  /** The resolved path inside the sandbox. */
20
53
  path: string;
@@ -1,26 +1,115 @@
1
1
  /**
2
- * A run's sandbox while the run holds it: a door into its screen or its shell,
3
- * and its working tree as it is right now.
2
+ * A run's sandbox. It outlives the run: kept after the run ends, parked (its pod
3
+ * stopped, its disk and working tree kept) while nobody watches, and retired a
4
+ * day after it parks; a free plan's sandbox is not parked, and ends within the
5
+ * hour. Resume gives a parked one a pod again under the same id and tree.
4
6
  *
5
- * POST /v1/sandbox/{id}/{screen|terminal}/ticket one ticket, spent when the page's socket opens (30 s)
6
- * POST /v1/sandbox/read {id, path} a directory's names, or a file's bytes
7
+ * GET /v1/sandbox/{id} its status and when its lease ends
8
+ * POST /v1/sandbox/{id}/pause park it: stop the pod, keep the disk
9
+ * POST /v1/sandbox/{id}/resume a pod again, same id and /work
10
+ * POST /v1/sandbox/{id}/{screen|terminal}/ticket one ticket, spent when the page's socket opens (30 s)
11
+ * GET /v1/sandbox/{id}/ports what listens in it, each with its preview host
12
+ * POST /v1/sandbox/{id}/preview {port} an address that opens that port in a browser
13
+ * POST /v1/sandbox/read {id, path} a directory's names, or a file's bytes
7
14
  *
8
- * The door is cloud's own page — noVNC for the screen, xterm for the shell — so
9
- * the builder frames it and never speaks the socket itself. The sandbox is gone
10
- * when the run ends, and every call here then answers 404.
15
+ * A door or a preview asked of a sandbox that stopped as it was asked is 409: it
16
+ * is resumed, and asked again once.
11
17
  */
12
18
  import { call, Refusal, seg } from './call.js';
19
+ const STATES = ['pending', 'running', 'parked', 'error'];
20
+ function box(raw, id) {
21
+ const o = raw && typeof raw === 'object' ? raw : {};
22
+ // A parked sandbox is also said to be paused; a status this does not know is not one to open.
23
+ const state = STATES.find((s) => s === (o.status === 'paused' ? 'parked' : o.status)) ?? 'error';
24
+ const ends = typeof o.expiresAt === 'number' && o.expiresAt > 0 ? o.expiresAt * 1000 : 0;
25
+ return { id, state, ends, error: typeof o.error === 'string' ? o.error : '' };
26
+ }
27
+ /** The sandbox as it stands. One the caller no longer holds is gone. */
28
+ export async function sandbox(t, id) {
29
+ try {
30
+ return box(await call(t, 'GET', `/v1/sandbox/${seg(id)}`), id);
31
+ }
32
+ catch (e) {
33
+ if (e instanceof Refusal && e.status === 404)
34
+ return { id, state: 'gone', ends: 0, error: '' };
35
+ throw e;
36
+ }
37
+ }
38
+ /** Park it: its pod stops, its disk and working tree stay. */
39
+ export async function park(t, id) {
40
+ return box(await call(t, 'POST', `/v1/sandbox/${seg(id)}/pause`), id);
41
+ }
42
+ /** Give it a pod again, under the same id and tree. */
43
+ export async function wake(t, id) {
44
+ return box(await call(t, 'POST', `/v1/sandbox/${seg(id)}/resume`), id);
45
+ }
46
+ /** How long a resumed sandbox is waited on to run. */
47
+ const UP = 90_000;
48
+ /** Resume it and wait until it runs. */
49
+ async function up(t, id) {
50
+ let b = await wake(t, id);
51
+ const until = Date.now() + UP;
52
+ while (b.state === 'pending' && Date.now() < until) {
53
+ await new Promise((r) => setTimeout(r, 1500));
54
+ b = await sandbox(t, id);
55
+ }
56
+ if (b.state !== 'running')
57
+ throw new Refusal(503, b.error || 'The sandbox did not start again. Try again.');
58
+ }
59
+ /** Asks what needs a running sandbox; a parked one is resumed first. */
60
+ async function running(t, id, ask) {
61
+ try {
62
+ return await ask();
63
+ }
64
+ catch (e) {
65
+ if (!(e instanceof Refusal) || e.status !== 409)
66
+ throw e;
67
+ await up(t, id);
68
+ return ask();
69
+ }
70
+ }
13
71
  /** A shell's tmux session name: the sandbox reattaches to it when the door opens again. */
14
72
  export const NAME = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$/;
15
73
  /** Where to frame one door, with a fresh ticket in its address. */
16
- export async function door(t, sandbox, which, session = '') {
17
- const r = await call(t, 'POST', `/v1/sandbox/${seg(sandbox)}/${which}/ticket`);
74
+ export async function door(t, id, which, session = '') {
75
+ const r = await running(t, id, () => call(t, 'POST', `/v1/sandbox/${seg(id)}/${which}/ticket`));
18
76
  const path = typeof r?.url === 'string' ? r.url : '';
19
77
  if (!path.startsWith('/v1/'))
20
78
  throw new Refusal(502, 'The sandbox answered with no address to open');
21
79
  const arg = which === 'terminal' && NAME.test(session) ? `&arg=${session}` : '';
22
80
  return `${t.api}${path}${arg}`;
23
81
  }
82
+ /** What listens in the sandbox, ascending. */
83
+ export async function ports(t, id) {
84
+ const r = await call(t, 'GET', `/v1/sandbox/${seg(id)}/ports`);
85
+ const list = Array.isArray(r?.ports) ? r.ports : [];
86
+ return list
87
+ .map((p) => (p && typeof p === 'object' ? p : {}))
88
+ .filter((p) => typeof p.port === 'number' && p.port > 0 && p.port < 65536)
89
+ .map((p) => ({
90
+ port: p.port,
91
+ host: typeof p.host === 'string' ? p.host : '',
92
+ }));
93
+ }
94
+ /**
95
+ * An address that opens one port in a browser: its own origin, with a single-use
96
+ * ticket that sets the preview's cookie and lands on its root.
97
+ */
98
+ export async function preview(t, id, port) {
99
+ const r = await running(t, id, () => call(t, 'POST', `/v1/sandbox/${seg(id)}/preview`, {
100
+ port,
101
+ }));
102
+ let url;
103
+ try {
104
+ url = new URL(typeof r?.url === 'string' ? r.url : '');
105
+ }
106
+ catch {
107
+ throw new Refusal(502, 'The sandbox answered with no address to open');
108
+ }
109
+ if (url.protocol !== 'https:')
110
+ throw new Refusal(502, 'The sandbox answered with no address to open');
111
+ return url.href;
112
+ }
24
113
  /** What a file shows at most. */
25
114
  const CAP = 1 << 20;
26
115
  /** One path in the sandbox, relative to its working directory. */
package/lib/api/turn.d.ts CHANGED
@@ -1,3 +1,26 @@
1
+ /**
2
+ * What a run's events SAY, as text a person reads.
3
+ *
4
+ * A curated projection, never a dump: an event carries the machinery a run
5
+ * happened on — hosts, paths, whole file bodies — and printing it would put
6
+ * someone's filesystem in front of anyone who can read the run. Each kind gets a
7
+ * sentence built from the few fields that describe the outcome; the rest is
8
+ * dropped by omission, because a projection cannot leak a field it never reads.
9
+ *
10
+ * Every string here is rendered as TEXT by the transcript. Nothing is HTML.
11
+ *
12
+ * The coding plane's vocabulary (apps/coding): `status` for a lifecycle move
13
+ * (started, routed, done, error, stopped, paused), `tool-call` for a step of the
14
+ * run itself, `event` for one line of the agent's own structured narration (the
15
+ * harness's JSON stream: its messages, reasoning, commands, edits, tool calls and
16
+ * plan), and `log` for raw output, which only the Terminal's log shows. A
17
+ * person's steering is a `control` {command, message}. A run on a project with a
18
+ * site ends `done` with `live` when it published what it pushed, or `unpublished`
19
+ * saying why it did not.
20
+ *
21
+ * `cards` is the transcript as it is drawn: the run's steps and the agent's
22
+ * items, never its log.
23
+ */
1
24
  import type { Event } from './sessions.ts';
2
25
  /** The payload as an object. A live frame may carry it as a JSON string. */
3
26
  export declare function decode(payload: unknown): Record<string, unknown> | string | null;
@@ -80,10 +103,11 @@ export type Ran = 'running' | 'done' | 'error' | 'cancelled';
80
103
  * One piece of the transcript, drawn by what it is.
81
104
  *
82
105
  * said prose: the agent's messages (markdown), or a person's steering words
106
+ * think the agent's reasoning, folded until opened
83
107
  * shell a command the agent ran, and what it printed
84
- * edit files the agent changed, with the diff when the harness printed one
85
- * read a file the agent read, drawn as a chip
108
+ * edit files the agent changed; the diff is the run's Git tab
86
109
  * step a step of the run itself (lease, clone, install, push), or a tool the agent called
110
+ * todo the agent's plan, as a checklist it ticks
87
111
  * note a lifecycle line: started, pushed, stopped
88
112
  * plan a plan run's answer, which can be approved into a build
89
113
  */
@@ -92,6 +116,10 @@ export type Card = {
92
116
  key: string;
93
117
  who: 'person' | 'agent';
94
118
  text: string;
119
+ } | {
120
+ kind: 'think';
121
+ key: string;
122
+ text: string;
95
123
  } | {
96
124
  kind: 'shell';
97
125
  key: string;
@@ -102,12 +130,6 @@ export type Card = {
102
130
  kind: 'edit';
103
131
  key: string;
104
132
  files: string[];
105
- patch: string;
106
- ran: Ran;
107
- } | {
108
- kind: 'read';
109
- key: string;
110
- file: string;
111
133
  ran: Ran;
112
134
  } | {
113
135
  kind: 'step';
@@ -116,6 +138,13 @@ export type Card = {
116
138
  detail: string;
117
139
  output: string;
118
140
  ran: Ran;
141
+ } | {
142
+ kind: 'todo';
143
+ key: string;
144
+ items: {
145
+ text: string;
146
+ done: boolean;
147
+ }[];
119
148
  } | {
120
149
  kind: 'note';
121
150
  key: string;
@@ -129,12 +158,14 @@ export type Card = {
129
158
  * A run's events, as the cards its transcript draws. `mode` is the run's, from
130
159
  * its record: a plan's final status is its plan, and nothing else draws a plan.
131
160
  *
132
- * The run's own steps arrive as `tool-call` {step, message, status}; each
133
- * command in the sandbox narrates its output as `log` {message} chunks and ends
134
- * with `tool-call` {step: "exit"}. The output after the step that starts the
135
- * agent is the agent's, read by its harness's grammar (harness.ts); after any
136
- * other step it is that step's. Once the agent has exited, the commands the run
137
- * runs before it pushes commit its work.
161
+ * The run's own steps arrive as `tool-call` {step, message, status}. The agent
162
+ * narrates itself as `event` lines: an item is started, updated and completed
163
+ * under one id, and its card is drawn once and kept current. Ids count from zero
164
+ * in each agent process, so an id is the agent's within the thread it started. A
165
+ * turn that fails is one plain line with the detail folded beneath it, drawn once
166
+ * however many times it is said; an `error` on its own is the agent retrying and
167
+ * is drawn only if the turn then fails. The final status carries the agent's
168
+ * answer, drawn once. `log` is never drawn here.
138
169
  */
139
170
  export declare function cards(events: Pick<Event, 'kind' | 'payload' | 'seq'>[], mode?: string): Card[];
140
171
  /** The plan a plan run answered with, from its final status, or ''. */