@tonbo/cli 0.3.4 → 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 +439 -53
- 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
|
@@ -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,6 +521,13 @@ 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;
|
|
@@ -562,7 +572,14 @@ var TonboApi = class {
|
|
|
562
572
|
}
|
|
563
573
|
).then((body) => body.organizations);
|
|
564
574
|
}
|
|
565
|
-
|
|
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
|
+
}) {
|
|
566
583
|
return requestJson(
|
|
567
584
|
this.fetcher,
|
|
568
585
|
`${this.accountOrigin}/api/cli/agents`,
|
|
@@ -575,14 +592,15 @@ var TonboApi = class {
|
|
|
575
592
|
body: JSON.stringify({
|
|
576
593
|
name,
|
|
577
594
|
requestId,
|
|
578
|
-
...organizationId ? { orgId: organizationId } : {}
|
|
595
|
+
...organizationId ? { orgId: organizationId } : {},
|
|
596
|
+
...region ? { region } : {}
|
|
579
597
|
})
|
|
580
598
|
}
|
|
581
599
|
).then((body) => body.agent).catch((cause) => {
|
|
582
600
|
if (cause instanceof Error && "status" in cause && typeof cause.status === "number" && cause.status >= 400 && cause.status < 500)
|
|
583
601
|
throw cause;
|
|
584
602
|
throw new Error(
|
|
585
|
-
`Agent creation was not confirmed. Query \`tonbo agent creation ${requestId}${organizationId ? ` --account ${organizationId}` : ""}\` or retry creation with --request-id ${requestId} and the same name and
|
|
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.`,
|
|
586
604
|
{ cause }
|
|
587
605
|
);
|
|
588
606
|
});
|
|
@@ -741,14 +759,13 @@ var TonboApi = class {
|
|
|
741
759
|
listRollouts(agentId, token) {
|
|
742
760
|
return this.managementList(`/v1/agents/${agentId}/production/rollouts`, token);
|
|
743
761
|
}
|
|
744
|
-
|
|
762
|
+
/** Checks that Production is running and opens a durable Session, or
|
|
763
|
+
* reuses the given one. */
|
|
764
|
+
async openSession({
|
|
745
765
|
agentId,
|
|
746
|
-
prompt,
|
|
747
766
|
sessionId,
|
|
748
|
-
token
|
|
749
|
-
turnId = randomUUID()
|
|
767
|
+
token
|
|
750
768
|
}) {
|
|
751
|
-
const agentPath = `/v1/agents/${agentId}`;
|
|
752
769
|
const production = await this.getProduction(agentId, token);
|
|
753
770
|
if (!production)
|
|
754
771
|
throw new Error("Agent has no Production Deployment; run `tonbo deploy` first.");
|
|
@@ -756,17 +773,42 @@ var TonboApi = class {
|
|
|
756
773
|
throw new Error(
|
|
757
774
|
`Production is ${production.observed_state}${production.last_error ? ` (${production.last_error})` : ""}; wait for it to be running.`
|
|
758
775
|
);
|
|
759
|
-
const session = sessionId ? { id: sessionId } : (await this.management(
|
|
760
|
-
deployment_id: production.deployment_id
|
|
761
|
-
})).data;
|
|
762
|
-
const turn = (await this.management(
|
|
776
|
+
const session = sessionId ? { id: sessionId } : (await this.management(
|
|
763
777
|
"POST",
|
|
764
|
-
|
|
778
|
+
`/v1/agents/${agentId}/sessions`,
|
|
765
779
|
token,
|
|
766
|
-
{
|
|
767
|
-
turnId
|
|
780
|
+
{ deployment_id: production.deployment_id }
|
|
768
781
|
)).data;
|
|
769
|
-
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);
|
|
770
812
|
}
|
|
771
813
|
turnEvents({
|
|
772
814
|
after = 0,
|
|
@@ -803,17 +845,24 @@ var TonboApi = class {
|
|
|
803
845
|
token
|
|
804
846
|
);
|
|
805
847
|
}
|
|
806
|
-
management(method, path6, token, body
|
|
807
|
-
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}`, {
|
|
808
859
|
method,
|
|
809
860
|
headers: {
|
|
810
861
|
authorization: `Bearer ${token}`,
|
|
811
|
-
...body === void 0 ? {} : {
|
|
812
|
-
"content-type": "application/json",
|
|
813
|
-
"idempotency-key": idempotencyKey ?? randomUUID()
|
|
814
|
-
}
|
|
862
|
+
...body === void 0 ? {} : { "content-type": "application/json", "idempotency-key": idempotencyKey }
|
|
815
863
|
},
|
|
816
|
-
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
864
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
865
|
+
...signal ? { signal } : {}
|
|
817
866
|
});
|
|
818
867
|
}
|
|
819
868
|
async managementList(path6, token) {
|
|
@@ -1107,13 +1156,15 @@ var AuthClient = class {
|
|
|
1107
1156
|
this.accountOrigin = accountOrigin;
|
|
1108
1157
|
this.browserOpener = browserOpener;
|
|
1109
1158
|
}
|
|
1110
|
-
|
|
1159
|
+
/** The stored access token, refreshed first when it has less than
|
|
1160
|
+
* `minimumLifetimeSeconds` left. */
|
|
1161
|
+
async accessToken(minimumLifetimeSeconds = 30) {
|
|
1111
1162
|
const injected = process.env.TONBO_ACCESS_TOKEN?.trim();
|
|
1112
1163
|
if (injected) return injected;
|
|
1113
1164
|
const tokens = await this.credentials.load();
|
|
1114
1165
|
if (!tokens) throw new Error("Not logged in. Run `tonbo login`.");
|
|
1115
1166
|
const legacyRelativeExpiry = !tokens.expires_at && tokens.expires_in;
|
|
1116
|
-
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))
|
|
1117
1168
|
return tokens.access_token;
|
|
1118
1169
|
if (!tokens.refresh_token) throw new Error("Tonbo login expired. Run `tonbo login` again.");
|
|
1119
1170
|
const config = await this.config();
|
|
@@ -1716,14 +1767,105 @@ function agentNameSuggestion(value) {
|
|
|
1716
1767
|
return name;
|
|
1717
1768
|
}
|
|
1718
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
|
+
|
|
1719
1861
|
// src/commands.ts
|
|
1720
|
-
async function resolveAgent(deps) {
|
|
1862
|
+
async function resolveAgent(deps, minimumTokenLifetimeSeconds) {
|
|
1721
1863
|
const root = await findDeclarationRoot(deps.cwd());
|
|
1722
1864
|
const declaration = await loadDeclaration(root);
|
|
1723
1865
|
if (!declaration.agent) {
|
|
1724
1866
|
throw new Error("No Agent is configured in .tonbo. Run `tonbo agent use <agent>` first.");
|
|
1725
1867
|
}
|
|
1726
|
-
const oauthToken = await deps.auth.accessToken();
|
|
1868
|
+
const oauthToken = await deps.auth.accessToken(minimumTokenLifetimeSeconds);
|
|
1727
1869
|
return {
|
|
1728
1870
|
declaration,
|
|
1729
1871
|
oauthToken,
|
|
@@ -1753,6 +1895,10 @@ async function initCommand(deps, options) {
|
|
|
1753
1895
|
const overwrite = options.force === true || exists && interactive;
|
|
1754
1896
|
if (exists && !overwrite)
|
|
1755
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
|
+
);
|
|
1756
1902
|
const inspection = await deps.inspectSource(root);
|
|
1757
1903
|
let harness = options.harness ?? (existingDeclaration ? "pi" : void 0);
|
|
1758
1904
|
if (harness !== void 0 && harness !== "pi") {
|
|
@@ -1799,7 +1945,7 @@ async function initCommand(deps, options) {
|
|
|
1799
1945
|
publicHostname: existingDeclaration.agent
|
|
1800
1946
|
} : { kind: "later" };
|
|
1801
1947
|
if (interactive && !existingDeclaration?.agent) {
|
|
1802
|
-
agentPlan = await selectInitAgent(deps, root);
|
|
1948
|
+
agentPlan = await selectInitAgent(deps, root, options.region);
|
|
1803
1949
|
}
|
|
1804
1950
|
if (interactive) {
|
|
1805
1951
|
while (true) {
|
|
@@ -1842,7 +1988,7 @@ async function initCommand(deps, options) {
|
|
|
1842
1988
|
...driver === "command" ? [{ description: entry, name: "PI SDK entry file", value: "entry" }] : [],
|
|
1843
1989
|
{ name: "Back to review", value: "back" }
|
|
1844
1990
|
]);
|
|
1845
|
-
if (setting === "agent") agentPlan = await selectInitAgent(deps, root);
|
|
1991
|
+
if (setting === "agent") agentPlan = await selectInitAgent(deps, root, options.region);
|
|
1846
1992
|
if (setting === "harness") harness = await selectHarness(deps);
|
|
1847
1993
|
if (setting === "driver") {
|
|
1848
1994
|
driver = await deps.select(
|
|
@@ -1871,7 +2017,11 @@ async function initCommand(deps, options) {
|
|
|
1871
2017
|
let createdAgent;
|
|
1872
2018
|
if (agentPlan.kind === "create") {
|
|
1873
2019
|
const oauthToken = await deps.auth.accessToken();
|
|
1874
|
-
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
|
+
});
|
|
1875
2025
|
agentHostname = createdAgent.publicHostname;
|
|
1876
2026
|
}
|
|
1877
2027
|
const configuredDeclaration = createDeclaration(
|
|
@@ -1904,9 +2054,11 @@ Build: ${buildCommand?.join(" ")}
|
|
|
1904
2054
|
Entrypoint: node ${entry}` : inspection.pi.packageCount > 0 ? `PI CLI with ${inspection.pi.packageCount} package${inspection.pi.packageCount === 1 ? "" : "s"}` : "PI CLI";
|
|
1905
2055
|
const agentSummary = createdAgent ? `Agent: ${createdAgent.name} (${createdAgent.publicHostname})
|
|
1906
2056
|
Organization: ${createdAgent.organizationName}
|
|
2057
|
+
Region: ${createdAgent.region}
|
|
1907
2058
|
Application: https://${createdAgent.publicHostname}
|
|
1908
2059
|
SSH after deploy: ssh ${createdAgent.sshDestination}` : agentPlan.kind === "existing" ? `Agent: ${agentPlan.name ? `${agentPlan.name} (${agentPlan.publicHostname})` : agentPlan.publicHostname}${agentPlan.organizationName ? `
|
|
1909
|
-
Organization: ${agentPlan.organizationName}` : ""}${agentPlan.
|
|
2060
|
+
Organization: ${agentPlan.organizationName}` : ""}${agentPlan.region ? `
|
|
2061
|
+
Region: ${agentPlan.region}` : ""}${agentPlan.sshDestination ? `
|
|
1910
2062
|
SSH after deploy: ssh ${agentPlan.sshDestination}` : ""}` : "Next: tonbo agent create <name>";
|
|
1911
2063
|
deps.output({
|
|
1912
2064
|
message: `${createdAgent ? `Created agent ${createdAgent.name} and` : exists ? "Updated" : "Created"} ${DECLARATION_FILENAME}.
|
|
@@ -1923,7 +2075,7 @@ function initSaveLabel(exists, agent) {
|
|
|
1923
2075
|
}
|
|
1924
2076
|
return exists ? `Replace ${DECLARATION_FILENAME}` : `Create ${DECLARATION_FILENAME}`;
|
|
1925
2077
|
}
|
|
1926
|
-
async function selectInitAgent(deps, root) {
|
|
2078
|
+
async function selectInitAgent(deps, root, requestedRegion) {
|
|
1927
2079
|
for (; ; ) {
|
|
1928
2080
|
const action = await deps.select("How should this directory connect to Tonbo?", [
|
|
1929
2081
|
{
|
|
@@ -1945,6 +2097,7 @@ async function selectInitAgent(deps, root) {
|
|
|
1945
2097
|
if (action === "later") return { kind: "later" };
|
|
1946
2098
|
if (action === "create") {
|
|
1947
2099
|
const organization = await selectInitOrganization(deps);
|
|
2100
|
+
const region = requestedRegion ?? await selectAgentRegion(deps);
|
|
1948
2101
|
const defaultName = agentNameSuggestion(path4.basename(root)) || "tonbo-agent";
|
|
1949
2102
|
for (; ; ) {
|
|
1950
2103
|
const answer = (await deps.prompt(`Agent name [${defaultName}]: `)).trim().toLowerCase();
|
|
@@ -1955,7 +2108,8 @@ async function selectInitAgent(deps, root) {
|
|
|
1955
2108
|
kind: "create",
|
|
1956
2109
|
name,
|
|
1957
2110
|
organizationId: organization?.id,
|
|
1958
|
-
organizationName: organization?.name
|
|
2111
|
+
organizationName: organization?.name,
|
|
2112
|
+
...region ? { region } : {}
|
|
1959
2113
|
};
|
|
1960
2114
|
} catch (error) {
|
|
1961
2115
|
deps.output({
|
|
@@ -1976,7 +2130,7 @@ async function selectInitAgent(deps, root) {
|
|
|
1976
2130
|
"Which agent should this directory use?",
|
|
1977
2131
|
agents.map((agent2) => ({
|
|
1978
2132
|
name: agent2.name,
|
|
1979
|
-
description: `Organization: ${agent2.organizationName} \xB7 ${agent2.publicHostname} \xB7 ${agent2.id}`,
|
|
2133
|
+
description: `Organization: ${agent2.organizationName} \xB7 ${agent2.region} \xB7 ${agent2.publicHostname} \xB7 ${agent2.id}`,
|
|
1980
2134
|
value: agent2.id
|
|
1981
2135
|
}))
|
|
1982
2136
|
);
|
|
@@ -1986,13 +2140,23 @@ async function selectInitAgent(deps, root) {
|
|
|
1986
2140
|
name: agent.name,
|
|
1987
2141
|
organizationName: agent.organizationName,
|
|
1988
2142
|
publicHostname: agent.publicHostname,
|
|
2143
|
+
region: agent.region,
|
|
1989
2144
|
sshDestination: agent.sshDestination
|
|
1990
2145
|
};
|
|
1991
2146
|
}
|
|
1992
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
|
+
}
|
|
1993
2157
|
function initAgentSummary(agent) {
|
|
1994
2158
|
if (agent.kind === "create")
|
|
1995
|
-
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)`;
|
|
1996
2160
|
if (agent.kind === "existing") {
|
|
1997
2161
|
return agent.name ? `${agent.name}${agent.organizationName ? ` in ${agent.organizationName}` : ""} (${agent.publicHostname})` : `Keep ${agent.publicHostname}`;
|
|
1998
2162
|
}
|
|
@@ -2166,7 +2330,13 @@ async function agentCreateCommand(deps, name, options = {}) {
|
|
|
2166
2330
|
const oauthToken = await deps.auth.accessToken();
|
|
2167
2331
|
validateAgentName(name);
|
|
2168
2332
|
const requestId = options.requestId ?? randomUUID3();
|
|
2169
|
-
const
|
|
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
|
+
});
|
|
2170
2340
|
try {
|
|
2171
2341
|
await bindDeclarationAgent(root, agent.publicHostname);
|
|
2172
2342
|
} catch (error) {
|
|
@@ -2176,7 +2346,13 @@ async function agentCreateCommand(deps, name, options = {}) {
|
|
|
2176
2346
|
);
|
|
2177
2347
|
}
|
|
2178
2348
|
deps.output({
|
|
2179
|
-
message:
|
|
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"),
|
|
2180
2356
|
agent,
|
|
2181
2357
|
requestId
|
|
2182
2358
|
});
|
|
@@ -2224,25 +2400,223 @@ async function deployCommand(deps, options = { promote: true }) {
|
|
|
2224
2400
|
}
|
|
2225
2401
|
deps.output({ message: lines.join("\n"), agent, ...result });
|
|
2226
2402
|
}
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
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
|
+
);
|
|
2235
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
|
+
}
|
|
2236
2573
|
deps.output({
|
|
2237
|
-
message:
|
|
2574
|
+
message: followed.completed.assistant_text,
|
|
2238
2575
|
agent,
|
|
2239
|
-
|
|
2576
|
+
production,
|
|
2577
|
+
session,
|
|
2578
|
+
turn: followed.completed
|
|
2240
2579
|
});
|
|
2241
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
|
+
}
|
|
2242
2616
|
async function agentListCommand(deps) {
|
|
2243
2617
|
const agents = await deps.api.listAgents(await deps.auth.accessToken());
|
|
2244
2618
|
deps.output({
|
|
2245
|
-
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.",
|
|
2246
2620
|
agents
|
|
2247
2621
|
});
|
|
2248
2622
|
}
|
|
@@ -2253,6 +2627,7 @@ async function agentShowCommand(deps) {
|
|
|
2253
2627
|
const lines = [
|
|
2254
2628
|
`agent: ${agent.name}`,
|
|
2255
2629
|
`Organization: ${agent.organizationName}`,
|
|
2630
|
+
`region: ${agent.region}`,
|
|
2256
2631
|
`application: https://${agent.publicHostname}`,
|
|
2257
2632
|
`ssh: ssh ${agent.sshDestination}`
|
|
2258
2633
|
];
|
|
@@ -2746,7 +3121,7 @@ function jsonOutput(program) {
|
|
|
2746
3121
|
}
|
|
2747
3122
|
function createProgram(dependencies = createDependencies) {
|
|
2748
3123
|
const program = new Command().name("tonbo").description("Deploy a persistent Agent to Tonbo.").version(packageVersion.version).option("--json", "print machine-readable JSON");
|
|
2749
|
-
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(
|
|
2750
3125
|
async (options) => initCommand(dependencies(jsonOutput(program)), options)
|
|
2751
3126
|
);
|
|
2752
3127
|
program.command("login").description("sign in through the browser and store the session in the user config").action(async () => loginCommand(dependencies(jsonOutput(program))));
|
|
@@ -2830,7 +3205,10 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2830
3205
|
);
|
|
2831
3206
|
});
|
|
2832
3207
|
const agent = program.command("agent").description("manage the Agent bound to this source directory");
|
|
2833
|
-
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(
|
|
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(
|
|
2834
3212
|
async (name, options) => agentCreateCommand(dependencies(jsonOutput(program)), name, options)
|
|
2835
3213
|
);
|
|
2836
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) => {
|
|
@@ -2870,9 +3248,17 @@ function createProgram(dependencies = createDependencies) {
|
|
|
2870
3248
|
deployments.command("rollback [deployment]").description("move Production back to the previous Production Deployment, or to a named one").action(
|
|
2871
3249
|
async (prefix) => deploymentsRollbackCommand(dependencies(jsonOutput(program)), prefix)
|
|
2872
3250
|
);
|
|
2873
|
-
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(
|
|
2874
3255
|
async (prompt, options) => runCommand(dependencies(jsonOutput(program)), prompt, options)
|
|
2875
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
|
+
);
|
|
2876
3262
|
const secret = program.command("secret").description("manage encrypted environment secrets for the agent service");
|
|
2877
3263
|
secret.command("list").action(async () => secretListCommand(dependencies(jsonOutput(program))));
|
|
2878
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(
|