@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 +19 -2
- package/dist/bin/tonbo.js +508 -63
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
778
|
+
`/v1/agents/${agentId}/sessions`,
|
|
743
779
|
token,
|
|
744
|
-
{
|
|
745
|
-
turnId
|
|
780
|
+
{ deployment_id: production.deployment_id }
|
|
746
781
|
)).data;
|
|
747
|
-
return { production, session
|
|
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
|
|
785
|
-
return
|
|
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
|
-
|
|
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) +
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
|
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({
|
|
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
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
token
|
|
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:
|
|
2574
|
+
message: followed.completed.assistant_text,
|
|
2208
2575
|
agent,
|
|
2209
|
-
|
|
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
|
|
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}.${
|
|
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 ??
|
|
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").
|
|
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
|
|
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(
|