@tonbo/cli 0.3.3 → 0.4.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.
package/README.md CHANGED
@@ -78,12 +78,29 @@ tonbo run "Reply with exactly READY."
78
78
 
79
79
  Choose `Use an existing agent` during `init` when the Agent already exists. `tonbo agent create <name>` and `tonbo agent use <name-hostname-or-id>` remain explicit automation and recovery commands.
80
80
 
81
+ Every Agent has a home region, chosen when it is created and never changed afterwards. Its Deployments, Sessions and files stay in that region. The hostname and the SSH destination are the same in every region. Name the region with `--region`; `tonbo region list` shows the regions that accept new resources (`allocation_enabled`). Without `--region`, Tonbo uses the only region that accepts new Agents. When several do, interactive `init` and `agent create` ask which one to use, and a non-interactive request without `--region` is refused with HTTP 400 (`region is required`). `init --region` applies only to an Agent that interactive `init` creates:
82
+
83
+ ```console
84
+ tonbo region list
85
+ tonbo agent create my-agent --region us-east-1
86
+ tonbo init --region us-east-1
87
+ ```
88
+
81
89
  `run` creates a durable Session and prints its ID in `--json` output. Pass that identity back to continue the same PI conversation:
82
90
 
83
91
  ```console
84
92
  tonbo run --session 4d8e9501-... "Continue the previous task."
85
93
  ```
86
94
 
95
+ Each `run` submits one durable Turn. Once the platform admits it, the Turn runs to its end whether or not the CLI keeps waiting, within the limits of `contracts/agents/turn-execution-v1.json` (today at most 60 minutes of execution, after waiting at most 70 minutes to start behind earlier Turns of the same Agent). `run` waits for the result as long as it takes: once the Turn is admitted it names the Turn and its Session on stderr and reads the Turn by id until it ends. It does not rely on the Management API's `202`, which comes only after a five-minute hold that outlasts how long Node's fetch waits for an answer; a submission that gets no answer before admission is sent again under the same id. Interrupting the wait, losing the network or signing out does not stop the Turn. `--no-wait` returns as soon as the Turn is admitted and prints how to collect its result later; `tonbo turn wait` does that from any bound directory whose signed-in account can use the Agent:
96
+
97
+ ```console
98
+ tonbo run --no-wait "Refactor the build and run the full test suite."
99
+ tonbo turn wait 7a000000-... --session 4d8e9501-...
100
+ ```
101
+
102
+ A failed Turn exits non-zero with its failure code, for example `Turn 7a000000-... failed (deadline_exceeded): ...`; the codes and their meaning are the `TurnFailureCode` schema of `contracts/management/openapi-v1.yaml`. `agent_busy` means the Agent already has the most Turns waiting to start and this one was not admitted; run it again later. Any other refusal also says the Turn was not admitted, and `run` stops sending a submission the platform has not admitted within 15 minutes. Waiting stops, without touching the Turn, a few minutes after the Turn's deadline if the platform has still not recorded its outcome.
103
+
87
104
  Every `tonbo deploy` creates one immutable Deployment: the uploaded source, the `.tonbo` settings and the git provenance of the source root (commit, branch, subject, author and whether the tree was dirty) when the directory is inside a git work tree. Git is optional; without it the Deployment records `tonbo deploy` as its source. Deployments are named by their commit subject, or by their short id (the first eight hex characters) when there is no commit, and every command that takes a Deployment accepts any unambiguous id prefix. The Agent's Production pointer names the one Deployment that serves its hostname; `deploy` moves Production to the new Deployment and prints the Deployment, its Production state and the exact standard OpenSSH command for the bound Agent and its username:
88
105
 
89
106
  ```console
@@ -117,7 +134,7 @@ tonbo deployments rollback 9b8c1d22 # or to a named one
117
134
 
118
135
  `tonbo deployments` (alias `tonbo deployments list`) marks the current Production Deployment with `●` and earlier ones with `○`; `STATUS` is the Deployment's relationship to Production: `current`, `promoting`, `failed`, `stopped`, `previous` or `none`. Promotion and rollback compare-and-swap the Production generation, so two people moving Production at once cannot silently overwrite each other; a conflict is reported and the command can be retried after checking `tonbo deployments`. `tonbo agent show` prints the Production Deployment and its state alongside the Agent addresses.
119
136
 
120
- Use `tonbo agent show` to rediscover the username, application address and SSH command later. `tonbo login` registers public keys found at the conventional `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub` and `id_rsa.pub` paths. Register a key at another path with `tonbo ssh-key add <path.pub>` and revoke a lost or retired key with `tonbo ssh-key remove SHA256:...`. The CLI is not part of the SSH connection; ordinary OpenSSH, `scp` and `rsync` connect directly:
137
+ Use `tonbo agent show` to rediscover the username, home region, application address and SSH command later. `tonbo login` registers public keys found at the conventional `~/.ssh/id_ed25519.pub`, `id_ecdsa.pub` and `id_rsa.pub` paths. Register a key at another path with `tonbo ssh-key add <path.pub>` and revoke a lost or retired key with `tonbo ssh-key remove SHA256:...`. The CLI is not part of the SSH connection; ordinary OpenSSH, `scp` and `rsync` connect directly:
121
138
 
122
139
  ```console
123
140
  ssh my-agent.my-account@tonbo.sh
@@ -217,7 +234,7 @@ tonbo --json machine unbind <machine-uuid>
217
234
  tonbo --json machine release <machine-uuid>
218
235
  ```
219
236
 
220
- Only one active Agent binding per Machine and one Machine per Agent are currently supported. Unbinding drains execution and completes physical deletion, final usage settlement and storage credential revocation before the Machine becomes available. Wait for `available: true` in `machine show` before rebinding. Cross-Agent reassignment resets rootfs; the destination mounts its own Agent Workspace and Sessions. Hidden pause/resume within the same Agent retains rootfs and process state. Release also drains an existing binding automatically; it does not delete Agent files or Sessions.
237
+ Only one active Agent binding per Machine and one Machine per Agent are currently supported. Unbinding drains execution and completes physical deletion, final usage settlement and storage credential revocation before the Machine becomes available. Wait for `available: true` in `machine show` before rebinding. Cross-Agent reassignment resets rootfs; the destination mounts its own Agent Workspace and Sessions. Hidden pause/resume within the same Agent retains rootfs and process state. Release also drains an existing binding automatically; it does not delete Agent files or Sessions. A Machine binds only to an Agent in the same region. `machine list` names any region it could not reach in `unavailable_regions`; its Machines are missing from that answer.
221
238
 
222
239
  `tonbo agent list` lists accessible Agents without requiring a local `.tonbo` file. JSON command output uses `agent` for one Agent and `agents` for the list; there is no duplicate `agent` output alias.
223
240
 
package/dist/bin/tonbo.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/app.ts
4
- import { randomUUID as randomUUID4 } from "node:crypto";
4
+ import { randomUUID as randomUUID5 } from "node:crypto";
5
5
  import { Command } from "commander";
6
6
  import { readFileSync } from "node:fs";
7
7
 
@@ -16,7 +16,7 @@ var HttpError = class extends Error {
16
16
  this.body = body;
17
17
  }
18
18
  };
19
- async function requestJson(fetcher, url, init = {}) {
19
+ async function requestJsonResponse(fetcher, url, init = {}) {
20
20
  const response = await fetcher(url, init);
21
21
  const body = await response.json().catch(() => null);
22
22
  if (!response.ok) {
@@ -30,7 +30,10 @@ async function requestJson(fetcher, url, init = {}) {
30
30
  body
31
31
  );
32
32
  }
33
- return body;
33
+ return { body, headers: response.headers, status: response.status };
34
+ }
35
+ async function requestJson(fetcher, url, init = {}) {
36
+ return (await requestJsonResponse(fetcher, url, init)).body;
34
37
  }
35
38
 
36
39
  // src/contracts.ts
@@ -518,12 +521,22 @@ function assertManagedDeploymentSpec(value) {
518
521
  }
519
522
 
520
523
  // src/api.ts
524
+ function turnSubmission(response) {
525
+ if (response.status === 202) return { state: "pending" };
526
+ const body = response.body ?? {};
527
+ if (!body.data || typeof body.data.assistant_text !== "string")
528
+ throw new Error("Tonbo answered the Turn without its result.");
529
+ return { state: "completed", turn: body.data };
530
+ }
521
531
  var TonboApi = class {
522
532
  constructor(fetcher, accountOrigin = "https://tonbo.dev", managementOrigin = "https://api.tonbo.dev") {
523
533
  this.fetcher = fetcher;
524
534
  this.accountOrigin = accountOrigin;
525
535
  this.managementOrigin = managementOrigin;
526
536
  }
537
+ cancelActivities(agentId, token, body) {
538
+ return this.management("POST", `/v1/agents/${agentId}/production/cancellations`, token, body);
539
+ }
527
540
  async listRegions(oauthToken) {
528
541
  return parseRegionDiscovery(
529
542
  await requestJson(this.fetcher, `${this.accountOrigin}/api/cli/regions`, {
@@ -559,7 +572,14 @@ var TonboApi = class {
559
572
  }
560
573
  ).then((body) => body.organizations);
561
574
  }
562
- createAgent(oauthToken, name, organizationId) {
575
+ /** An omitted region lets the server choose the only region that accepts
576
+ * new Agents; once more than one does, it refuses the request. */
577
+ createAgent(oauthToken, {
578
+ name,
579
+ organizationId,
580
+ region,
581
+ requestId = randomUUID()
582
+ }) {
563
583
  return requestJson(
564
584
  this.fetcher,
565
585
  `${this.accountOrigin}/api/cli/agents`,
@@ -569,9 +589,29 @@ var TonboApi = class {
569
589
  authorization: `Bearer ${oauthToken}`,
570
590
  "content-type": "application/json"
571
591
  },
572
- body: JSON.stringify(organizationId ? { name, orgId: organizationId } : { name })
592
+ body: JSON.stringify({
593
+ name,
594
+ requestId,
595
+ ...organizationId ? { orgId: organizationId } : {},
596
+ ...region ? { region } : {}
597
+ })
573
598
  }
574
- ).then((body) => body.agent);
599
+ ).then((body) => body.agent).catch((cause) => {
600
+ if (cause instanceof Error && "status" in cause && typeof cause.status === "number" && cause.status >= 400 && cause.status < 500)
601
+ throw cause;
602
+ throw new Error(
603
+ `Agent creation was not confirmed. Query \`tonbo agent creation ${requestId}${organizationId ? ` --account ${organizationId}` : ""}\` or retry creation with --request-id ${requestId} and the same name, account and region.`,
604
+ { cause }
605
+ );
606
+ });
607
+ }
608
+ readAgentCreationRequest(oauthToken, requestId, accountId) {
609
+ const account = accountId ? `?account=${encodeURIComponent(accountId)}` : "";
610
+ return requestJson(
611
+ this.fetcher,
612
+ `${this.accountOrigin}/api/cli/agents/creations/${encodeURIComponent(requestId)}${account}`,
613
+ { headers: { authorization: `Bearer ${oauthToken}` } }
614
+ );
575
615
  }
576
616
  registerSshKey(oauthToken, key) {
577
617
  return requestJson(
@@ -719,14 +759,13 @@ var TonboApi = class {
719
759
  listRollouts(agentId, token) {
720
760
  return this.managementList(`/v1/agents/${agentId}/production/rollouts`, token);
721
761
  }
722
- async run({
762
+ /** Checks that Production is running and opens a durable Session, or
763
+ * reuses the given one. */
764
+ async openSession({
723
765
  agentId,
724
- prompt,
725
766
  sessionId,
726
- token,
727
- turnId = randomUUID()
767
+ token
728
768
  }) {
729
- const agentPath = `/v1/agents/${agentId}`;
730
769
  const production = await this.getProduction(agentId, token);
731
770
  if (!production)
732
771
  throw new Error("Agent has no Production Deployment; run `tonbo deploy` first.");
@@ -734,17 +773,42 @@ var TonboApi = class {
734
773
  throw new Error(
735
774
  `Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
736
775
  );
737
- const session = sessionId ? { id: sessionId } : (await this.management("POST", `${agentPath}/sessions`, token, {
738
- deployment_id: production.deployment_id
739
- })).data;
740
- const turn = (await this.management(
776
+ const session = sessionId ? { id: sessionId } : (await this.management(
741
777
  "POST",
742
- `${agentPath}/sessions/${session.id}/turns`,
778
+ `/v1/agents/${agentId}/sessions`,
743
779
  token,
744
- { prompt },
745
- turnId
780
+ { deployment_id: production.deployment_id }
746
781
  )).data;
747
- return { production, session, turn };
782
+ return { production, session };
783
+ }
784
+ /** Submits one Turn under its own id; sending it again under the same id is
785
+ * answered from the same operation. The Management API holds the request
786
+ * for up to five minutes: it answers the result, 409 `turn_failed`, a
787
+ * refusal, or 202 while the Turn waits or runs. */
788
+ async submitTurn({
789
+ agentId,
790
+ prompt,
791
+ sessionId,
792
+ signal,
793
+ token,
794
+ turnId
795
+ }) {
796
+ return turnSubmission(
797
+ await this.managementResponse(
798
+ "POST",
799
+ `/v1/agents/${agentId}/sessions/${sessionId}/turns`,
800
+ token,
801
+ { body: { prompt }, idempotencyKey: turnId, signal }
802
+ )
803
+ );
804
+ }
805
+ /** One durable Turn; reading it never affects its execution. */
806
+ getTurn(agentId, sessionId, turnId, token) {
807
+ return this.management(
808
+ "GET",
809
+ `/v1/agents/${agentId}/sessions/${sessionId}/turns/${turnId}`,
810
+ token
811
+ ).then((response) => response.data);
748
812
  }
749
813
  turnEvents({
750
814
  after = 0,
@@ -781,17 +845,24 @@ var TonboApi = class {
781
845
  token
782
846
  );
783
847
  }
784
- management(method, path6, token, body, idempotencyKey) {
785
- return requestJson(this.fetcher, `${this.managementOrigin}${path6}`, {
848
+ management(method, path6, token, body) {
849
+ return this.managementResponse(method, path6, token, { body }).then(
850
+ (response) => response.body
851
+ );
852
+ }
853
+ managementResponse(method, path6, token, {
854
+ body,
855
+ idempotencyKey = randomUUID(),
856
+ signal
857
+ } = {}) {
858
+ return requestJsonResponse(this.fetcher, `${this.managementOrigin}${path6}`, {
786
859
  method,
787
860
  headers: {
788
861
  authorization: `Bearer ${token}`,
789
- ...body === void 0 ? {} : {
790
- "content-type": "application/json",
791
- "idempotency-key": idempotencyKey ?? randomUUID()
792
- }
862
+ ...body === void 0 ? {} : { "content-type": "application/json", "idempotency-key": idempotencyKey }
793
863
  },
794
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
864
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
865
+ ...signal ? { signal } : {}
795
866
  });
796
867
  }
797
868
  async managementList(path6, token) {
@@ -1085,13 +1156,15 @@ var AuthClient = class {
1085
1156
  this.accountOrigin = accountOrigin;
1086
1157
  this.browserOpener = browserOpener;
1087
1158
  }
1088
- async accessToken() {
1159
+ /** The stored access token, refreshed first when it has less than
1160
+ * `minimumLifetimeSeconds` left. */
1161
+ async accessToken(minimumLifetimeSeconds = 30) {
1089
1162
  const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
1090
1163
  if (injected) return injected;
1091
1164
  const tokens = await this.credentials.load();
1092
1165
  if (!tokens) throw new Error("Not logged in. Run `tonbo login`.");
1093
1166
  const legacyRelativeExpiry = !tokens.expires_at && tokens.expires_in;
1094
- if (!legacyRelativeExpiry && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1e3) + 30))
1167
+ if (!legacyRelativeExpiry && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1e3) + minimumLifetimeSeconds))
1095
1168
  return tokens.access_token;
1096
1169
  if (!tokens.refresh_token) throw new Error("Tonbo login expired. Run `tonbo login` again.");
1097
1170
  const config = await this.config();
@@ -1257,8 +1330,9 @@ async function openBrowser(url) {
1257
1330
 
1258
1331
  // ../../packages/agent-source-bundler/dist/src/generated/contracts.js
1259
1332
  var inferenceDefault = { "model": "claude-sonnet-5" };
1260
- var sourceBundleContract = { "version": 1, "format": "tar-v1", "key_prefix": "agent-source-bundles", "content_type": "application/vnd.tonbo.source+tar", "max_bytes": 67108864 };
1333
+ var sourceBundleContract = { "version": 1, "format": "tar-v1", "key_prefix": "agent-source-bundles", "content_type": "application/vnd.tonbo.source+tar", "max_bytes": 67108864, "runtime_cache_directories": [".pi/npm", ".pi/git"] };
1261
1334
  var piSessionContract = { "version": 1, "adapter": "pi-jsonl-v3", "format_version": 3, "durable_completion_timeout_seconds": 30, "turn_claim_seconds": 30, "turn_claim_renew_seconds": 10, "session_directory": "/sessions", "path_template": "/sessions/{session_id}.jsonl", "preflight_command": ["/usr/local/bin/artifacts", "runtime", "session-preflight"] };
1335
+ var piSlackTemplate = { "repository": "tonbo-io/pi-slack-agent", "source_directory": "examples/pi-slack-agent", "service": { "command": ["node", "slack/server.mjs"], "secrets": ["SLACK_SIGNING_SECRET", "SLACK_BOT_TOKEN", "SLACK_BOT_USER_ID", "SLACK_TEAM_ID", "TONBO_AGENT_API_KEY", "TONBO_AGENT_API_ORIGIN", "TONBO_IAM_ORIGIN"] } };
1262
1336
 
1263
1337
  // ../../packages/agent-source-bundler/dist/src/inference-default.js
1264
1338
  var DEFAULT_INFERENCE_MODEL = inferenceDefault.model;
@@ -1293,6 +1367,7 @@ function githubRepositoryUrl(remote) {
1293
1367
 
1294
1368
  // src/commands.ts
1295
1369
  import path4 from "node:path";
1370
+ import { randomUUID as randomUUID3 } from "node:crypto";
1296
1371
 
1297
1372
  // src/build.ts
1298
1373
  import { spawn } from "node:child_process";
@@ -1321,11 +1396,13 @@ async function runBuildCommand(root, command) {
1321
1396
 
1322
1397
  // ../../packages/agent-source-bundler/dist/src/native-pi.js
1323
1398
  import { stringify } from "smol-toml";
1324
- function nativePiDeclaration(model) {
1399
+ var PI_SLACK_TEMPLATE_SERVICE = piSlackTemplate.service;
1400
+ function nativePiDeclaration(model, service) {
1325
1401
  return {
1326
1402
  version: 2,
1327
1403
  harness: { runtime: "pi", driver: { kind: "native" } },
1328
- inference: { model }
1404
+ inference: { model },
1405
+ ...service ? { service } : {}
1329
1406
  };
1330
1407
  }
1331
1408
 
@@ -1447,8 +1524,7 @@ var DEFAULT_IGNORES = [
1447
1524
  "!.env.example",
1448
1525
  ".tonbo-cache/",
1449
1526
  ".tonbo-system/",
1450
- ".pi/npm/",
1451
- ".pi/git/"
1527
+ ...sourceBundleContract.runtime_cache_directories.map((directory) => `${directory}/`)
1452
1528
  ];
1453
1529
  var EXACT_NPM_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
1454
1530
  var GIT_COMMIT = /^[0-9a-f]{40}$/i;
@@ -1691,14 +1767,105 @@ function agentNameSuggestion(value) {
1691
1767
  return name;
1692
1768
  }
1693
1769
 
1770
+ // src/turns.ts
1771
+ var TURN_OUTCOME_GRACE_MS = 3 * 60 * 1e3;
1772
+ var POLL_MIN_MS = 1e3;
1773
+ var POLL_MAX_MS = 1e4;
1774
+ var READ_FAILURE_LIMIT = 10;
1775
+ var TurnFailedError = class extends Error {
1776
+ constructor(turnId, failureCode, detail) {
1777
+ super(
1778
+ `Turn ${turnId} failed${failureCode ? ` (${failureCode})` : ""}${detail ? `: ${detail}` : "."}`
1779
+ );
1780
+ this.turnId = turnId;
1781
+ this.failureCode = failureCode;
1782
+ this.detail = detail;
1783
+ }
1784
+ };
1785
+ function problemCode(error) {
1786
+ if (!(error instanceof HttpError)) return null;
1787
+ const body = error.body && typeof error.body === "object" ? error.body : {};
1788
+ const code = body.code;
1789
+ return typeof code === "string" ? code : null;
1790
+ }
1791
+ function submissionProblem(error, turnId) {
1792
+ if (!(error instanceof HttpError)) return error;
1793
+ const body = error.body ?? {};
1794
+ const code = problemCode(error);
1795
+ if (error.status === 409 && code === "turn_failed")
1796
+ return new TurnFailedError(
1797
+ turnId,
1798
+ typeof body.failure_code === "string" ? body.failure_code : null,
1799
+ typeof body.detail === "string" ? body.detail : null
1800
+ );
1801
+ if (error.status === 409 && code === "agent_busy")
1802
+ return new Error(
1803
+ `Turn ${turnId} was not admitted: the Agent already has the most Turns waiting to start (agent_busy). Try again with a new Turn later.`,
1804
+ { cause: error }
1805
+ );
1806
+ return new Error(`Turn ${turnId} was not admitted: ${error.message}`, { cause: error });
1807
+ }
1808
+ function isUnauthenticated(error) {
1809
+ return error instanceof HttpError && error.status === 401;
1810
+ }
1811
+ function isNotFound(error) {
1812
+ return error instanceof HttpError && error.status === 404;
1813
+ }
1814
+ function isTransient(error) {
1815
+ if (error instanceof HttpError) return error.status === 429 || error.status >= 500;
1816
+ return error instanceof TypeError;
1817
+ }
1818
+ function turnWaitBound(turn) {
1819
+ const deadline = turn.deadline_at ?? turn.start_deadline_at;
1820
+ if (!deadline) return null;
1821
+ const at = Date.parse(deadline);
1822
+ return Number.isFinite(at) ? at + TURN_OUTCOME_GRACE_MS : null;
1823
+ }
1824
+ async function followTurn({
1825
+ now,
1826
+ read: read2,
1827
+ resume,
1828
+ sleep,
1829
+ turnId
1830
+ }) {
1831
+ let delay = POLL_MIN_MS;
1832
+ let failures = 0;
1833
+ for (; ; ) {
1834
+ let state;
1835
+ try {
1836
+ state = await read2();
1837
+ failures = 0;
1838
+ } catch (error) {
1839
+ const transient = isTransient(error);
1840
+ if (transient && (failures += 1) < READ_FAILURE_LIMIT) {
1841
+ await sleep(POLL_MAX_MS);
1842
+ continue;
1843
+ }
1844
+ if (!transient && !(error instanceof HttpError)) throw error;
1845
+ throw new Error(
1846
+ `${error instanceof Error ? error.message : String(error)} Waiting stopped, which does not stop the Turn; ${resume}`,
1847
+ { cause: error }
1848
+ );
1849
+ }
1850
+ if ("result" in state) return state.result;
1851
+ const bound = state.pending ? turnWaitBound(state.pending) : null;
1852
+ if (bound !== null && now() > bound)
1853
+ throw new Error(
1854
+ `Turn ${turnId} is still pending after its deadline, and the platform has not recorded its outcome. Waiting stopped, which does not stop the Turn; ${resume}`
1855
+ );
1856
+ await sleep(delay);
1857
+ delay = Math.min(delay * 2, POLL_MAX_MS);
1858
+ }
1859
+ }
1860
+
1694
1861
  // src/commands.ts
1695
- async function resolveAgent(deps) {
1862
+ async function resolveAgent(deps, minimumTokenLifetimeSeconds) {
1696
1863
  const root = await findDeclarationRoot(deps.cwd());
1697
1864
  const declaration = await loadDeclaration(root);
1698
1865
  if (!declaration.agent) {
1699
1866
  throw new Error("No Agent is configured in .tonbo. Run `tonbo agent use <agent>` first.");
1700
1867
  }
1701
- const oauthToken = await deps.auth.accessToken();
1868
+ const oauthToken = await deps.auth.accessToken(minimumTokenLifetimeSeconds);
1702
1869
  return {
1703
1870
  declaration,
1704
1871
  oauthToken,
@@ -1728,6 +1895,10 @@ async function initCommand(deps, options) {
1728
1895
  const overwrite = options.force === true || exists && interactive;
1729
1896
  if (exists && !overwrite)
1730
1897
  throw new Error(`${DECLARATION_FILENAME} already exists. Pass --force to replace it.`);
1898
+ if (options.region !== void 0 && !interactive)
1899
+ throw new Error(
1900
+ "--region applies only to an agent that interactive init creates. Run `tonbo agent create <name> --region <region>` instead."
1901
+ );
1731
1902
  const inspection = await deps.inspectSource(root);
1732
1903
  let harness = options.harness ?? (existingDeclaration ? "pi" : void 0);
1733
1904
  if (harness !== void 0 && harness !== "pi") {
@@ -1774,7 +1945,7 @@ async function initCommand(deps, options) {
1774
1945
  publicHostname: existingDeclaration.agent
1775
1946
  } : { kind: "later" };
1776
1947
  if (interactive && !existingDeclaration?.agent) {
1777
- agentPlan = await selectInitAgent(deps, root);
1948
+ agentPlan = await selectInitAgent(deps, root, options.region);
1778
1949
  }
1779
1950
  if (interactive) {
1780
1951
  while (true) {
@@ -1817,7 +1988,7 @@ async function initCommand(deps, options) {
1817
1988
  ...driver === "command" ? [{ description: entry, name: "PI SDK entry file", value: "entry" }] : [],
1818
1989
  { name: "Back to review", value: "back" }
1819
1990
  ]);
1820
- if (setting === "agent") agentPlan = await selectInitAgent(deps, root);
1991
+ if (setting === "agent") agentPlan = await selectInitAgent(deps, root, options.region);
1821
1992
  if (setting === "harness") harness = await selectHarness(deps);
1822
1993
  if (setting === "driver") {
1823
1994
  driver = await deps.select(
@@ -1846,7 +2017,11 @@ async function initCommand(deps, options) {
1846
2017
  let createdAgent;
1847
2018
  if (agentPlan.kind === "create") {
1848
2019
  const oauthToken = await deps.auth.accessToken();
1849
- createdAgent = await deps.api.createAgent(oauthToken, agentPlan.name, agentPlan.organizationId);
2020
+ createdAgent = await deps.api.createAgent(oauthToken, {
2021
+ name: agentPlan.name,
2022
+ organizationId: agentPlan.organizationId,
2023
+ region: agentPlan.region
2024
+ });
1850
2025
  agentHostname = createdAgent.publicHostname;
1851
2026
  }
1852
2027
  const configuredDeclaration = createDeclaration(
@@ -1879,9 +2054,11 @@ Build: ${buildCommand?.join(" ")}
1879
2054
  Entrypoint: node ${entry}` : inspection.pi.packageCount > 0 ? `PI CLI with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}` : "PI CLI";
1880
2055
  const agentSummary = createdAgent ? `Agent: ${createdAgent.name} (${createdAgent.publicHostname})
1881
2056
  Organization: ${createdAgent.organizationName}
2057
+ Region: ${createdAgent.region}
1882
2058
  Application: https://${createdAgent.publicHostname}
1883
2059
  SSH after deploy: ssh ${createdAgent.sshDestination}` : agentPlan.kind === "existing" ? `Agent: ${agentPlan.name ? `${agentPlan.name} (${agentPlan.publicHostname})` : agentPlan.publicHostname}${agentPlan.organizationName ? `
1884
- Organization: ${agentPlan.organizationName}` : ""}${agentPlan.sshDestination ? `
2060
+ Organization: ${agentPlan.organizationName}` : ""}${agentPlan.region ? `
2061
+ Region: ${agentPlan.region}` : ""}${agentPlan.sshDestination ? `
1885
2062
  SSH after deploy: ssh ${agentPlan.sshDestination}` : ""}` : "Next: tonbo agent create <name>";
1886
2063
  deps.output({
1887
2064
  message: `${createdAgent ? `Created agent ${createdAgent.name} and` : exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
@@ -1898,7 +2075,7 @@ function initSaveLabel(exists, agent) {
1898
2075
  }
1899
2076
  return exists ? `Replace ${DECLARATION_FILENAME}` : `Create ${DECLARATION_FILENAME}`;
1900
2077
  }
1901
- async function selectInitAgent(deps, root) {
2078
+ async function selectInitAgent(deps, root, requestedRegion) {
1902
2079
  for (; ; ) {
1903
2080
  const action = await deps.select("How should this directory connect to Tonbo?", [
1904
2081
  {
@@ -1920,6 +2097,7 @@ async function selectInitAgent(deps, root) {
1920
2097
  if (action === "later") return { kind: "later" };
1921
2098
  if (action === "create") {
1922
2099
  const organization = await selectInitOrganization(deps);
2100
+ const region = requestedRegion ?? await selectAgentRegion(deps);
1923
2101
  const defaultName = agentNameSuggestion(path4.basename(root)) || "tonbo-agent";
1924
2102
  for (; ; ) {
1925
2103
  const answer = (await deps.prompt(`Agent name [${defaultName}]: `)).trim().toLowerCase();
@@ -1930,7 +2108,8 @@ async function selectInitAgent(deps, root) {
1930
2108
  kind: "create",
1931
2109
  name,
1932
2110
  organizationId: organization?.id,
1933
- organizationName: organization?.name
2111
+ organizationName: organization?.name,
2112
+ ...region ? { region } : {}
1934
2113
  };
1935
2114
  } catch (error) {
1936
2115
  deps.output({
@@ -1951,7 +2130,7 @@ async function selectInitAgent(deps, root) {
1951
2130
  "Which agent should this directory use?",
1952
2131
  agents.map((agent2) => ({
1953
2132
  name: agent2.name,
1954
- description: `Organization: ${agent2.organizationName} \xB7 ${agent2.publicHostname} \xB7 ${agent2.id}`,
2133
+ description: `Organization: ${agent2.organizationName} \xB7 ${agent2.region} \xB7 ${agent2.publicHostname} \xB7 ${agent2.id}`,
1955
2134
  value: agent2.id
1956
2135
  }))
1957
2136
  );
@@ -1961,13 +2140,23 @@ async function selectInitAgent(deps, root) {
1961
2140
  name: agent.name,
1962
2141
  organizationName: agent.organizationName,
1963
2142
  publicHostname: agent.publicHostname,
2143
+ region: agent.region,
1964
2144
  sshDestination: agent.sshDestination
1965
2145
  };
1966
2146
  }
1967
2147
  }
2148
+ async function selectAgentRegion(deps) {
2149
+ const { regions } = await deps.api.listRegions(await deps.auth.accessToken());
2150
+ const open3 = regions.filter((region) => region.allocation_enabled);
2151
+ if (open3.length < 2) return void 0;
2152
+ return deps.select(
2153
+ "Which region should hold the new agent? It stays there permanently.",
2154
+ open3.map((region) => ({ description: region.id, name: region.display_name, value: region.id }))
2155
+ );
2156
+ }
1968
2157
  function initAgentSummary(agent) {
1969
2158
  if (agent.kind === "create")
1970
- return `Create ${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""} (permanent address assigned after creation)`;
2159
+ return `Create ${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""}${agent.region ? `, region ${agent.region}` : ""} (permanent address assigned after creation)`;
1971
2160
  if (agent.kind === "existing") {
1972
2161
  return agent.name ? `${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""} (${agent.publicHostname})` : `Keep ${agent.publicHostname}`;
1973
2162
  }
@@ -2130,7 +2319,7 @@ async function agentUseCommand(deps, selector, force = false) {
2130
2319
  await bindDeclarationAgent(root, agent.publicHostname);
2131
2320
  deps.output({ message: `Bound .tonbo to agent ${agent.publicHostname}.`, agent });
2132
2321
  }
2133
- async function agentCreateCommand(deps, name) {
2322
+ async function agentCreateCommand(deps, name, options = {}) {
2134
2323
  const root = await findDeclarationRoot(deps.cwd());
2135
2324
  const declaration = await loadDeclaration(root);
2136
2325
  if (declaration.agent) {
@@ -2140,7 +2329,14 @@ async function agentCreateCommand(deps, name) {
2140
2329
  }
2141
2330
  const oauthToken = await deps.auth.accessToken();
2142
2331
  validateAgentName(name);
2143
- const agent = await deps.api.createAgent(oauthToken, name);
2332
+ const requestId = options.requestId ?? randomUUID3();
2333
+ const region = options.region ?? (deps.interactive() ? await selectAgentRegion(deps) : void 0);
2334
+ const agent = await deps.api.createAgent(oauthToken, {
2335
+ name,
2336
+ organizationId: options.account,
2337
+ region,
2338
+ requestId
2339
+ });
2144
2340
  try {
2145
2341
  await bindDeclarationAgent(root, agent.publicHostname);
2146
2342
  } catch (error) {
@@ -2149,7 +2345,17 @@ async function agentCreateCommand(deps, name) {
2149
2345
  { cause: error }
2150
2346
  );
2151
2347
  }
2152
- deps.output({ message: `Created agent ${agent.name} and bound it in .tonbo.`, agent });
2348
+ deps.output({
2349
+ message: [
2350
+ `Created agent ${agent.name} and bound it in .tonbo.`,
2351
+ `Organization: ${agent.organizationName}`,
2352
+ `Region: ${agent.region}`,
2353
+ `Application: https://${agent.publicHostname}`,
2354
+ `SSH after deploy: ssh ${agent.sshDestination}`
2355
+ ].join("\n"),
2356
+ agent,
2357
+ requestId
2358
+ });
2153
2359
  }
2154
2360
  async function deployCommand(deps, options = { promote: true }) {
2155
2361
  const { declaration, oauthToken, agent, root } = await resolveAgent(deps);
@@ -2194,25 +2400,223 @@ async function deployCommand(deps, options = { promote: true }) {
2194
2400
  }
2195
2401
  deps.output({ message: lines.join("\n"), agent, ...result });
2196
2402
  }
2197
- async function runCommand(deps, prompt, options) {
2198
- const { oauthToken, agent } = await resolveAgent(deps);
2199
- const managementToken = await deps.api.exchangeManagementToken(oauthToken, agent.id);
2200
- const result = await deps.api.run({
2201
- agentId: agent.id,
2202
- prompt,
2203
- sessionId: options.session,
2204
- token: managementToken
2403
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2404
+ var ADMISSION_WAIT_MS = 15 * 60 * 1e3;
2405
+ var RESUBMIT_MS = 5e3;
2406
+ var SUBMISSION_TOKEN_LIFETIME_SECONDS = 240;
2407
+ function managementTokens(deps, agentId, oauthToken) {
2408
+ let current = deps.api.exchangeManagementToken(oauthToken, agentId);
2409
+ return async function withToken(call) {
2410
+ const token = await current;
2411
+ try {
2412
+ return await call(token);
2413
+ } catch (error) {
2414
+ if (!isUnauthenticated(error)) throw error;
2415
+ current = deps.auth.accessToken().then((oauth) => deps.api.exchangeManagementToken(oauth, agentId));
2416
+ return call(await current);
2417
+ }
2418
+ };
2419
+ }
2420
+ function waitCommand(turnId, sessionId) {
2421
+ return `tonbo turn wait ${turnId} --session ${sessionId}`;
2422
+ }
2423
+ function turnNotFound(turnId, sessionId, agentName) {
2424
+ return new Error(`Turn ${turnId} was not found in Session ${sessionId} of agent ${agentName}.`);
2425
+ }
2426
+ function sessionTurn(turn) {
2427
+ return {
2428
+ assistant_text: turn.assistant_text ?? "",
2429
+ session_id: turn.session_id,
2430
+ turn_id: turn.id
2431
+ };
2432
+ }
2433
+ function holdSubmission(deps, withToken, submission) {
2434
+ const controller = new AbortController();
2435
+ const held = { controller, settled: Promise.resolve() };
2436
+ held.settled = withToken(
2437
+ (token) => deps.api.submitTurn({ ...submission, signal: controller.signal, token })
2438
+ ).then(
2439
+ (value) => {
2440
+ held.answer = { value };
2441
+ },
2442
+ (error) => {
2443
+ held.answer = { error };
2444
+ }
2445
+ );
2446
+ return held;
2447
+ }
2448
+ async function submitAndFollow(deps, withToken, submission, { agentName, outcome }) {
2449
+ const { agentId, sessionId, turnId } = submission;
2450
+ const now = deps.now ?? Date.now;
2451
+ const sleep = deps.sleep ?? pause;
2452
+ const submittedAt = now();
2453
+ const state = {
2454
+ admitted: false,
2455
+ announced: false,
2456
+ held: holdSubmission(deps, withToken, submission),
2457
+ resubmitAt: 0,
2458
+ unanswered: null
2459
+ };
2460
+ const read2 = async () => {
2461
+ const answer = state.held?.answer;
2462
+ if (answer) {
2463
+ state.held = null;
2464
+ if ("error" in answer) {
2465
+ const problem = submissionProblem(answer.error, turnId);
2466
+ if (problem instanceof TurnFailedError) throw problem;
2467
+ if (!state.admitted) {
2468
+ if (!isTransient(answer.error)) throw problem;
2469
+ state.unanswered = answer.error;
2470
+ state.resubmitAt = now() + RESUBMIT_MS;
2471
+ }
2472
+ } else if (answer.value.state === "completed") {
2473
+ return { result: { completed: answer.value.turn } };
2474
+ }
2475
+ }
2476
+ let turn = null;
2477
+ try {
2478
+ turn = await withToken((token) => deps.api.getTurn(agentId, sessionId, turnId, token));
2479
+ } catch (error) {
2480
+ if (!isNotFound(error)) throw error;
2481
+ if (state.admitted) throw turnNotFound(turnId, sessionId, agentName);
2482
+ }
2483
+ if (turn) {
2484
+ state.admitted = true;
2485
+ if (turn.status === "completed") return { result: { completed: sessionTurn(turn) } };
2486
+ if (turn.status === "failed")
2487
+ throw new TurnFailedError(turn.id, turn.failure_code, turn.failure);
2488
+ if (!outcome) return { result: { admitted: turn } };
2489
+ if (!state.announced) {
2490
+ state.announced = true;
2491
+ deps.progress.start(
2492
+ `Turn ${turnId} is running on the platform in Session ${sessionId}; waiting for its result. Interrupting the wait does not stop the Turn.`
2493
+ );
2494
+ }
2495
+ return { pending: turn };
2496
+ }
2497
+ if (now() - submittedAt > ADMISSION_WAIT_MS) {
2498
+ const cause = state.unanswered;
2499
+ throw new Error(
2500
+ `Turn ${turnId} was not admitted within ${ADMISSION_WAIT_MS / 6e4} minutes${cause instanceof Error ? ` (the last submission got no answer: ${cause.message})` : ""}. Waiting stopped; if the platform admits it later, \`${waitCommand(turnId, sessionId)}\` follows it.`,
2501
+ { cause }
2502
+ );
2503
+ }
2504
+ if (!state.held && now() >= state.resubmitAt)
2505
+ state.held = holdSubmission(deps, withToken, submission);
2506
+ return { pending: null };
2507
+ };
2508
+ try {
2509
+ const followed = await followTurn({
2510
+ now,
2511
+ read: read2,
2512
+ resume: `wait for it with \`${waitCommand(turnId, sessionId)}\`.`,
2513
+ // An answer to the held submission ends a pause at once.
2514
+ sleep: async (ms) => {
2515
+ const held = state.held;
2516
+ if (!held) return sleep(ms);
2517
+ const wake = new AbortController();
2518
+ await Promise.race([sleep(ms, wake.signal), held.settled]);
2519
+ wake.abort();
2520
+ },
2521
+ turnId
2522
+ });
2523
+ if (state.announced) deps.progress.succeed(`Turn ${turnId} completed.`);
2524
+ return followed;
2525
+ } catch (error) {
2526
+ if (state.announced) deps.progress.fail();
2527
+ throw error;
2528
+ } finally {
2529
+ state.held?.controller.abort();
2530
+ }
2531
+ }
2532
+ function pause(ms, signal) {
2533
+ return new Promise((resolve) => {
2534
+ const timer = setTimeout(resolve, ms);
2535
+ signal?.addEventListener(
2536
+ "abort",
2537
+ () => {
2538
+ clearTimeout(timer);
2539
+ resolve();
2540
+ },
2541
+ { once: true }
2542
+ );
2205
2543
  });
2544
+ }
2545
+ async function runCommand(deps, prompt, options) {
2546
+ const { oauthToken, agent } = await resolveAgent(deps, SUBMISSION_TOKEN_LIFETIME_SECONDS);
2547
+ const withToken = managementTokens(deps, agent.id, oauthToken);
2548
+ const { production, session } = await withToken(
2549
+ (token) => deps.api.openSession({ agentId: agent.id, sessionId: options.session, token })
2550
+ );
2551
+ const turnId = randomUUID3();
2552
+ const followed = await submitAndFollow(
2553
+ deps,
2554
+ withToken,
2555
+ { agentId: agent.id, prompt, sessionId: session.id, turnId },
2556
+ { agentName: agent.name, outcome: options.wait !== false }
2557
+ );
2558
+ if ("admitted" in followed) {
2559
+ deps.output({
2560
+ message: [
2561
+ `Turn ${turnId} was admitted in Session ${session.id}; the platform runs it to its end.`,
2562
+ "Wait for its result:",
2563
+ ` ${waitCommand(turnId, session.id)}`
2564
+ ].join("\n"),
2565
+ agent,
2566
+ production,
2567
+ session,
2568
+ turn_id: turnId,
2569
+ turn: followed.admitted
2570
+ });
2571
+ return;
2572
+ }
2206
2573
  deps.output({
2207
- message: result.turn.assistant_text,
2574
+ message: followed.completed.assistant_text,
2208
2575
  agent,
2209
- ...result
2576
+ production,
2577
+ session,
2578
+ turn: followed.completed
2210
2579
  });
2211
2580
  }
2581
+ async function turnWaitCommand(deps, turnId, options) {
2582
+ if (!UUID.test(turnId) || !UUID.test(options.session))
2583
+ throw new Error("The Turn and its Session are identified by UUIDs.");
2584
+ const { oauthToken, agent } = await resolveAgent(deps);
2585
+ const withToken = managementTokens(deps, agent.id, oauthToken);
2586
+ deps.progress.start(`Waiting for Turn ${turnId}. Interrupting the wait does not stop the Turn.`);
2587
+ try {
2588
+ const turn = await followTurn({
2589
+ now: deps.now ?? Date.now,
2590
+ read: async () => {
2591
+ let current;
2592
+ try {
2593
+ current = await withToken(
2594
+ (token) => deps.api.getTurn(agent.id, options.session, turnId, token)
2595
+ );
2596
+ } catch (error) {
2597
+ if (!isNotFound(error)) throw error;
2598
+ throw turnNotFound(turnId, options.session, agent.name);
2599
+ }
2600
+ if (current.status === "completed") return { result: current };
2601
+ if (current.status === "failed")
2602
+ throw new TurnFailedError(current.id, current.failure_code, current.failure);
2603
+ return { pending: current };
2604
+ },
2605
+ resume: `run \`${waitCommand(turnId, options.session)}\` again.`,
2606
+ sleep: deps.sleep ?? pause,
2607
+ turnId
2608
+ });
2609
+ deps.progress.succeed(`Turn ${turnId} completed.`);
2610
+ deps.output({ message: turn.assistant_text ?? "", agent, turn });
2611
+ } catch (error) {
2612
+ deps.progress.fail();
2613
+ throw error;
2614
+ }
2615
+ }
2212
2616
  async function agentListCommand(deps) {
2213
2617
  const agents = await deps.api.listAgents(await deps.auth.accessToken());
2214
2618
  deps.output({
2215
- message: agents.length ? agents.map((agent) => `${agent.name} \xB7 ${agent.publicHostname}`).join("\n") : "No agents are available.",
2619
+ message: agents.length ? agents.map((agent) => `${agent.name} \xB7 ${agent.publicHostname} \xB7 ${agent.region}`).join("\n") : "No agents are available.",
2216
2620
  agents
2217
2621
  });
2218
2622
  }
@@ -2223,6 +2627,7 @@ async function agentShowCommand(deps) {
2223
2627
  const lines = [
2224
2628
  `agent: ${agent.name}`,
2225
2629
  `Organization: ${agent.organizationName}`,
2630
+ `region: ${agent.region}`,
2226
2631
  `application: https://${agent.publicHostname}`,
2227
2632
  `ssh: ssh ${agent.sshDestination}`
2228
2633
  ];
@@ -2441,9 +2846,23 @@ async function secretRemoveCommand(deps, name) {
2441
2846
  await deps.api.deleteAgentSecret(agent.id, name, token);
2442
2847
  deps.output({ message: `Removed agent secret ${name}.`, agent, name });
2443
2848
  }
2849
+ async function cancelActivitiesCommand(deps, options) {
2850
+ const { oauthToken, agent } = await resolveAgent(deps);
2851
+ const token = await deps.api.exchangeManagementToken(oauthToken, agent.id);
2852
+ const result = await deps.api.cancelActivities(agent.id, token, {
2853
+ sandbox_id: options.sandbox,
2854
+ runtime_session_id: options.runtimeSession,
2855
+ work_keys: options.workKey,
2856
+ reason: options.reason
2857
+ });
2858
+ deps.output({
2859
+ message: "Activity cancellation requested. Physical retirement and application checkpoint cancellation must still be verified.",
2860
+ cancellation: result
2861
+ });
2862
+ }
2444
2863
 
2445
2864
  // src/credentials.ts
2446
- import { randomUUID as randomUUID3 } from "node:crypto";
2865
+ import { randomUUID as randomUUID4 } from "node:crypto";
2447
2866
  import { chmod, lstat as lstat4, mkdir, open as open2, readFile as readFile5, rename as rename2, rm as rm2 } from "node:fs/promises";
2448
2867
  import os from "node:os";
2449
2868
  import path5 from "node:path";
@@ -2474,7 +2893,7 @@ var FileCredentialStore = class {
2474
2893
  await assertExistingDestinationIsSafe(this.filename);
2475
2894
  const temporary = path5.join(
2476
2895
  directory,
2477
- `.${path5.basename(this.filename)}.${process.pid}.${randomUUID3()}.tmp`
2896
+ `.${path5.basename(this.filename)}.${process.pid}.${randomUUID4()}.tmp`
2478
2897
  );
2479
2898
  let handle = null;
2480
2899
  try {
@@ -2702,7 +3121,7 @@ function jsonOutput(program) {
2702
3121
  }
2703
3122
  function createProgram(dependencies = createDependencies) {
2704
3123
  const program = new Command().name("tonbo").description("Deploy a persistent Agent to Tonbo.").version(packageVersion.version).option("--json", "print machine-readable JSON");
2705
- program.command("init").description("interactively create a Tonbo Agent declaration in this directory").option("--harness <harness>", "Agent Harness (currently pi)").option("--model <model>", "inference model").option("--driver <driver>", "PI driver: native or command").option("--agent-entry <file>", "Node entry file for the command driver").option("--build-command <argv...>", "build command argv for the command driver").option("--force", `replace an existing ${DECLARATION_FILENAME}`).action(
3124
+ program.command("init").description("interactively create a Tonbo Agent declaration in this directory").option("--harness <harness>", "Agent Harness (currently pi)").option("--model <model>", "inference model").option("--driver <driver>", "PI driver: native or command").option("--agent-entry <file>", "Node entry file for the command driver").option("--build-command <argv...>", "build command argv for the command driver").option("--region <region>", "home region of an agent init creates (see tonbo region list)").option("--force", `replace an existing ${DECLARATION_FILENAME}`).action(
2706
3125
  async (options) => initCommand(dependencies(jsonOutput(program)), options)
2707
3126
  );
2708
3127
  program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(jsonOutput(program))));
@@ -2725,7 +3144,7 @@ function createProgram(dependencies = createDependencies) {
2725
3144
  "stable allocation request UUID; reuse it after an uncertain response"
2726
3145
  ).action(async (options) => {
2727
3146
  const deps = dependencies(jsonOutput(program));
2728
- const requestId = options.requestId ?? randomUUID4();
3147
+ const requestId = options.requestId ?? randomUUID5();
2729
3148
  try {
2730
3149
  const result = await deps.api.machineRequest(await deps.auth.accessToken(), "", "POST", {
2731
3150
  accountId: options.account,
@@ -2786,7 +3205,22 @@ function createProgram(dependencies = createDependencies) {
2786
3205
  );
2787
3206
  });
2788
3207
  const agent = program.command("agent").description("manage the Agent bound to this source directory");
2789
- agent.command("create <name>").description("create an agent and bind this directory to it").action(async (name) => agentCreateCommand(dependencies(jsonOutput(program)), name));
3208
+ agent.command("create <name>").description("create an agent and bind this directory to it").option("--account <account>", "owning account UUID; defaults to your personal account").option(
3209
+ "--region <region>",
3210
+ "permanent home region (see tonbo region list); required once several regions accept new agents"
3211
+ ).option("--request-id <uuid>", "reuse the same creation UUID after an uncertain response").action(
3212
+ async (name, options) => agentCreateCommand(dependencies(jsonOutput(program)), name, options)
3213
+ );
3214
+ agent.command("creation <requestId>").description("query the original Agent for a creation request").option("--account <account>", "owning account UUID; defaults to your personal account").action(async (requestId, options) => {
3215
+ const deps = dependencies(jsonOutput(program));
3216
+ deps.output(
3217
+ await deps.api.readAgentCreationRequest(
3218
+ await deps.auth.accessToken(),
3219
+ requestId,
3220
+ options.account
3221
+ )
3222
+ );
3223
+ });
2790
3224
  agent.command("list").description("list the agents available to your account").action(async () => agentListCommand(dependencies(jsonOutput(program))));
2791
3225
  const sshKey = program.command("ssh-key").description("manage public keys used by native agent SSH");
2792
3226
  sshKey.command("add <public-key>").description("register an OpenSSH public key with the current Tonbo account").action(async (path6) => sshKeyAddCommand(dependencies(jsonOutput(program)), path6));
@@ -2808,12 +3242,23 @@ function createProgram(dependencies = createDependencies) {
2808
3242
  deployments.command("promote <deployment>").description("move Production to a Deployment by id prefix").action(
2809
3243
  async (prefix) => deploymentsPromoteCommand(dependencies(jsonOutput(program)), prefix)
2810
3244
  );
3245
+ deployments.command("cancel-activities").description("cancel selected named work on an exact retired runtime").requiredOption("--sandbox <id>", "retired Sandbox UUID").requiredOption("--runtime-session <id>", "exact activation UUID").requiredOption("--work-key <keys...>", "named work keys to cancel").requiredOption("--reason <reason>", "reason retained in the cancellation audit").action(
3246
+ async (options) => cancelActivitiesCommand(dependencies(jsonOutput(program)), options)
3247
+ );
2811
3248
  deployments.command("rollback [deployment]").description("move Production back to the previous Production Deployment, or to a named one").action(
2812
3249
  async (prefix) => deploymentsRollbackCommand(dependencies(jsonOutput(program)), prefix)
2813
3250
  );
2814
- program.command("run <prompt>").description("run one prompt in a durable agent session").option("--session <session>", "resume an existing Agent Session UUID").action(
3251
+ program.command("run <prompt>").description("run one prompt as a durable Turn in an agent session and wait for its result").option("--session <session>", "resume an existing Agent Session UUID").option(
3252
+ "--no-wait",
3253
+ "return once the Turn is admitted; the platform runs it to its end (see tonbo turn wait)"
3254
+ ).action(
2815
3255
  async (prompt, options) => runCommand(dependencies(jsonOutput(program)), prompt, options)
2816
3256
  );
3257
+ program.command("turn").description("follow durable Turns of the bound agent").command("wait <turn>").description(
3258
+ "wait for a Turn's result; the Turn runs on the platform whether or not anyone waits"
3259
+ ).requiredOption("--session <session>", "the Turn's Agent Session UUID").action(
3260
+ async (turnId, options) => turnWaitCommand(dependencies(jsonOutput(program)), turnId, options)
3261
+ );
2817
3262
  const secret = program.command("secret").description("manage encrypted environment secrets for the agent service");
2818
3263
  secret.command("list").action(async () => secretListCommand(dependencies(jsonOutput(program))));
2819
3264
  secret.command("set <name>").description("set a secret from an environment variable (the same name by default)").option("--from-env <name>", "read the value from another environment variable").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonbo/cli",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Deploy persistent Agents and manage their Machines from the command line.",
5
5
  "homepage": "https://tonbo.dev",
6
6
  "bugs": {