@tangle-network/agent-app 0.49.16 → 0.49.18
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/dist/hosted-agent/index.d.ts +29 -116
- package/dist/hosted-agent/index.js +71 -252
- package/dist/hosted-agent/index.js.map +1 -1
- package/package.json +5 -10
|
@@ -1,45 +1,27 @@
|
|
|
1
|
-
import { type HubProviderEvent } from '@tangle-network/hub-sdk';
|
|
2
1
|
import type { AgentProfile } from '@tangle-network/sandbox';
|
|
2
|
+
import { type LineAttachment } from '@tangle-network/sandbox/core';
|
|
3
3
|
export * from './engine';
|
|
4
4
|
/**
|
|
5
|
-
* A hosted agent:
|
|
6
|
-
*
|
|
5
|
+
* A hosted agent: people text or call a line, and each person is answered
|
|
6
|
+
* from their own isolated sandbox. The developer's Tangle API key pays for
|
|
7
7
|
* every box, model turn and reply.
|
|
8
8
|
*
|
|
9
|
-
* The platform
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* carries over from the store.
|
|
16
|
-
*
|
|
17
|
-
* Every step is idempotent by the message's turn id: a retried delivery
|
|
18
|
-
* settles the same sandbox turn, the same allowance admission and the same
|
|
19
|
-
* Hub send. So the caller may retry any failure, and needs no dedupe table.
|
|
9
|
+
* The platform does the work. {@link HostedAgent.attachLine} attaches the
|
|
10
|
+
* line to Tangle Hub with one sandbox per person: Hub routes each text to the
|
|
11
|
+
* sender's box (the named instance {@link PERSON_KEY_PREFIX} plus a hash of
|
|
12
|
+
* their number), keeps one thread per person, handles STOP and START, counts
|
|
13
|
+
* each person's texts per day, and sends the reply. A call through ph0ny runs
|
|
14
|
+
* in the same box and the same thread, so text and voice share one memory.
|
|
20
15
|
*/
|
|
21
|
-
/** Durable key-value storage for
|
|
16
|
+
/** Durable key-value storage for voice call tokens. A Cloudflare KV namespace satisfies it. */
|
|
22
17
|
export interface HostedAgentStore {
|
|
23
18
|
get(key: string): Promise<string | null>;
|
|
24
19
|
put(key: string, value: string, options?: {
|
|
25
20
|
expirationTtl?: number;
|
|
26
21
|
}): Promise<void>;
|
|
27
22
|
}
|
|
28
|
-
export type HostedChannel = 'imessage' | 'voice';
|
|
29
|
-
export interface HostedMessage {
|
|
30
|
-
/** The sender's address as the channel delivered it: E.164 for a phone. */
|
|
31
|
-
userId: string;
|
|
32
|
-
channel: HostedChannel;
|
|
33
|
-
text: string;
|
|
34
|
-
/** Stable per logical message. Reusing it settles the same turn. */
|
|
35
|
-
turnId: string;
|
|
36
|
-
}
|
|
37
|
-
/** What the allowance hook decides for one new message. */
|
|
38
|
-
export type Allowance = 'answer' | 'ignore' | {
|
|
39
|
-
reply: string;
|
|
40
|
-
};
|
|
41
23
|
export interface HostedAgentConfig {
|
|
42
|
-
/** The developer's Tangle API key
|
|
24
|
+
/** The developer's Tangle API key. */
|
|
43
25
|
apiKey: string;
|
|
44
26
|
/**
|
|
45
27
|
* The persona every person's box runs. A profile without `model.default`
|
|
@@ -50,39 +32,19 @@ export interface HostedAgentConfig {
|
|
|
50
32
|
profile: AgentProfile;
|
|
51
33
|
/** Backend harness type, such as `opencode`; the runtime default when omitted. */
|
|
52
34
|
harness?: string;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
* `hub.allowances`). Each person is one member. Default `hosted-agent`.
|
|
57
|
-
*/
|
|
58
|
-
meter?: string;
|
|
59
|
-
/**
|
|
60
|
-
* Answers per person per UTC day before `allow` must decide. Default 20.
|
|
61
|
-
* Written to the meter's plan when the meter has none; a plan already set
|
|
62
|
-
* on the Platform wins.
|
|
63
|
-
*/
|
|
35
|
+
/** The owner's own phone (E.164), the line's first member. */
|
|
36
|
+
owner: string;
|
|
37
|
+
/** Texts Hub answers per person per UTC day. Default 20. */
|
|
64
38
|
freeTurnsPerDay?: number;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
* checkout link. Without it, or past the paid allowance, a person is told to
|
|
69
|
-
* come back tomorrow. Retries of an admitted message never reach this hook.
|
|
70
|
-
*/
|
|
71
|
-
allow?: (message: HostedMessage, usedToday: number) => Allowance | Promise<Allowance>;
|
|
72
|
-
/**
|
|
73
|
-
* The owner's own address (E.164 phone). Only this sender can text DEBUG ON
|
|
74
|
-
* or DEBUG OFF; while on, each reply to them ends with one ⚙ line naming the
|
|
75
|
-
* harness, model, box, time, tokens, cost and tools of that turn.
|
|
76
|
-
*/
|
|
77
|
-
owner?: string;
|
|
39
|
+
box?: Partial<BoxPolicy>;
|
|
40
|
+
/** Voice call tokens. Required for `voiceHook` and `voiceAsk`. */
|
|
41
|
+
store?: HostedAgentStore;
|
|
78
42
|
/** Shared secret for the ph0ny call hook and `ask_workspace` webhook tool. */
|
|
79
43
|
voiceSecret?: string;
|
|
80
44
|
/** How long one `ask_workspace` call may wait before it returns a ticket. Default 8 s. */
|
|
81
45
|
voiceBudgetMs?: number;
|
|
82
|
-
box?: Partial<BoxPolicy>;
|
|
83
46
|
turnWallCapMs?: number;
|
|
84
47
|
sandboxUrl?: string;
|
|
85
|
-
hubUrl?: string;
|
|
86
48
|
}
|
|
87
49
|
export interface BoxPolicy {
|
|
88
50
|
cpuCores: number;
|
|
@@ -95,6 +57,12 @@ export interface BoxPolicy {
|
|
|
95
57
|
allowDomains: string[];
|
|
96
58
|
}
|
|
97
59
|
export declare const DEFAULT_BOX_POLICY: BoxPolicy;
|
|
60
|
+
/**
|
|
61
|
+
* Each person's box is the developer's named instance with this prefix. It is
|
|
62
|
+
* the key this kit used before Hub routed its texts, so every existing
|
|
63
|
+
* person keeps their box.
|
|
64
|
+
*/
|
|
65
|
+
export declare const PERSON_KEY_PREFIX = "hosted:";
|
|
98
66
|
/**
|
|
99
67
|
* Harness tools a texting or calling assistant does not use. Their
|
|
100
68
|
* descriptions present every turn as coding work: a shell, file search,
|
|
@@ -108,21 +76,6 @@ export declare const CONVERSATION_TOOLS_OFF: readonly ['bash', 'glob', 'grep', '
|
|
|
108
76
|
* (2026-09-23), within the latency of the others.
|
|
109
77
|
*/
|
|
110
78
|
export declare const DEFAULT_HOSTED_MODEL = "openai/gpt-5.6-luna";
|
|
111
|
-
/** What one turn reports about itself, for the owner's debug line. A figure
|
|
112
|
-
* the run did not report stays undefined and is left out of the line. */
|
|
113
|
-
export interface TurnTrace {
|
|
114
|
-
/** Time the platform took to hand over the person's running box, such as `box 0.3s`. */
|
|
115
|
-
box?: string;
|
|
116
|
-
harness?: string;
|
|
117
|
-
model?: string;
|
|
118
|
-
/** Sandbox clock: the run's own start to finish. */
|
|
119
|
-
runMs?: number;
|
|
120
|
-
inputTokens?: number;
|
|
121
|
-
outputTokens?: number;
|
|
122
|
-
reasoningTokens?: number;
|
|
123
|
-
costUsd?: number;
|
|
124
|
-
tools?: number;
|
|
125
|
-
}
|
|
126
79
|
export type AskResult = {
|
|
127
80
|
state: 'answered';
|
|
128
81
|
text: string;
|
|
@@ -132,59 +85,19 @@ export type AskResult = {
|
|
|
132
85
|
} | {
|
|
133
86
|
state: 'pending';
|
|
134
87
|
};
|
|
135
|
-
/** A Hub delivery that passed authentication, in a form a queue can carry. */
|
|
136
|
-
export interface HostedInbound {
|
|
137
|
-
runId: string;
|
|
138
|
-
connectionId: string;
|
|
139
|
-
event: HubProviderEvent;
|
|
140
|
-
}
|
|
141
|
-
export declare const NOTICE: {
|
|
142
|
-
readonly debugOn: 'Debug on. Each reply now ends with a ⚙ line: agent, harness, model, box, time, tokens, cost, tools and turn. Text DEBUG OFF to stop.';
|
|
143
|
-
readonly debugOff: 'Debug off. Replies no longer carry the ⚙ line.';
|
|
144
|
-
readonly stopped: 'You are unsubscribed. Text START to talk again.';
|
|
145
|
-
readonly started: 'Welcome back. Text me anytime. Reply STOP to stop.';
|
|
146
|
-
readonly limit: 'You have used today\'s free messages. Talk tomorrow!';
|
|
147
|
-
readonly unavailable: 'Sorry, I could not answer that just now. Please try again in a minute.';
|
|
148
|
-
};
|
|
149
|
-
/** One address per person: E.164 for anything phone-shaped, else lowercase. */
|
|
150
|
-
export declare function normalizeAddress(value: string): string | null;
|
|
151
|
-
/** `true` for DEBUG ON, `false` for DEBUG OFF, `null` for any other text. */
|
|
152
|
-
export declare function debugCommand(text: string): boolean | null;
|
|
153
|
-
/** The owner's ⚙ line. `totalMs` runs from the provider's receipt of the text to now. */
|
|
154
|
-
export declare function debugFooter(agent: string, turnId: string, trace: TurnTrace, totalMs?: number): string;
|
|
155
88
|
export declare class HostedAgentError extends Error {
|
|
156
89
|
readonly code: string;
|
|
157
90
|
constructor(code: string, message: string);
|
|
158
91
|
}
|
|
159
92
|
export declare function createHostedAgent(config: HostedAgentConfig): {
|
|
160
|
-
ask: (message: HostedMessage, options: {
|
|
161
|
-
deadline: number;
|
|
162
|
-
trace?: TurnTrace;
|
|
163
|
-
}) => Promise<AskResult>;
|
|
164
|
-
/**
|
|
165
|
-
* One-time setup: route an Inkbox iMessage identity's messages, connected
|
|
166
|
-
* to Hub under the developer's account, to `callbackUrl`, and allow the
|
|
167
|
-
* one reply action on that connection. Safe to repeat.
|
|
168
|
-
*/
|
|
169
|
-
connect(input: {
|
|
170
|
-
connectionId: string;
|
|
171
|
-
identityId: string;
|
|
172
|
-
callbackUrl: string;
|
|
173
|
-
secret: string;
|
|
174
|
-
}): Promise<import("@tangle-network/hub-sdk").HubEventSubscription>;
|
|
175
|
-
/** The Hub callback. Authenticates within Hub's 10 s budget and runs nothing. */
|
|
176
|
-
receive(request: Request, secret: string): Promise<{
|
|
177
|
-
response: Response;
|
|
178
|
-
inbound?: HostedInbound;
|
|
179
|
-
}>;
|
|
180
93
|
/**
|
|
181
|
-
*
|
|
182
|
-
* '
|
|
183
|
-
*
|
|
94
|
+
* Attach an Inkbox iMessage identity, connected to Hub under the
|
|
95
|
+
* developer's account, as this agent's line: the owner and anyone who
|
|
96
|
+
* texts it each get their own box and thread. Safe to repeat with the
|
|
97
|
+
* same config. Remove any Hub event subscription on the connection first;
|
|
98
|
+
* Hub refuses a line that another route would also answer.
|
|
184
99
|
*/
|
|
185
|
-
|
|
186
|
-
lastAttempt?: boolean;
|
|
187
|
-
}): Promise<'replied' | 'ignored' | 'pending'>;
|
|
100
|
+
attachLine(connectionId: string): Promise<LineAttachment>;
|
|
188
101
|
/**
|
|
189
102
|
* ph0ny's call hook. `admit` returns a call token bound to the caller's
|
|
190
103
|
* number from the carrier's signed call record, never from the model.
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// src/hosted-agent/index.ts
|
|
2
|
-
import {
|
|
3
|
-
import { buildMessagingReply, normalizeConversationEvent } from "@tangle-network/agent-integrations/conversation-events";
|
|
4
|
-
import { InstanceRestartingError, Sandbox } from "@tangle-network/sandbox/core";
|
|
2
|
+
import { InstanceRestartingError, lineInstanceKey, Sandbox } from "@tangle-network/sandbox/core";
|
|
5
3
|
|
|
6
4
|
// src/hosted-agent/engine.ts
|
|
7
5
|
var MAX_MESSAGE_CHARS = 8e3;
|
|
@@ -174,6 +172,7 @@ var DEFAULT_BOX_POLICY = {
|
|
|
174
172
|
deleteAfterStoppedSeconds: 7 * 86400,
|
|
175
173
|
allowDomains: ["router.tangle.tools"]
|
|
176
174
|
};
|
|
175
|
+
var PERSON_KEY_PREFIX = "hosted:";
|
|
177
176
|
var CONVERSATION_TOOLS_OFF = ["bash", "glob", "grep", "task", "todowrite", "webfetch", "skill"];
|
|
178
177
|
var DEFAULT_HOSTED_MODEL = "openai/gpt-5.6-luna";
|
|
179
178
|
function conversationProfile(profile) {
|
|
@@ -189,24 +188,11 @@ function conversationProfile(profile) {
|
|
|
189
188
|
}
|
|
190
189
|
};
|
|
191
190
|
}
|
|
192
|
-
var
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
stopped: "You are unsubscribed. Text START to talk again.",
|
|
196
|
-
started: "Welcome back. Text me anytime. Reply STOP to stop.",
|
|
197
|
-
limit: "You have used today's free messages. Talk tomorrow!",
|
|
198
|
-
unavailable: "Sorry, I could not answer that just now. Please try again in a minute."
|
|
199
|
-
};
|
|
200
|
-
var IMESSAGE_EVENT = "inkbox.imessage.received";
|
|
201
|
-
var IMESSAGE_REPLY = "inkbox.imessage.reply";
|
|
202
|
-
var STOP = /^(stop|stopall|unsubscribe|cancel|end|quit)$/i;
|
|
203
|
-
var START = /^(start|unstop|resume)$/i;
|
|
204
|
-
var CARRY_CHARS = 6e3;
|
|
205
|
-
var CARRY_HEAD = "[Your conversation with this person so far. Your instructions may have changed since; follow the current ones.]";
|
|
206
|
-
var CARRY_NOW = "[Their new message:]";
|
|
191
|
+
var PERSON = { context: "own", tools: "act" };
|
|
192
|
+
var CALL_TTL_MS = 2 * 36e5;
|
|
193
|
+
var E164 = /^\+[1-9]\d{6,14}$/;
|
|
207
194
|
var encode = (text) => new TextEncoder().encode(text);
|
|
208
|
-
var
|
|
209
|
-
var sha256 = async (value) => hex(await crypto.subtle.digest("SHA-256", encode(value)));
|
|
195
|
+
var sha256 = async (value) => Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", encode(value))), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
210
196
|
async function equal(given, expected) {
|
|
211
197
|
if (!given) return false;
|
|
212
198
|
const [a, b] = await Promise.all([sha256(given), sha256(expected)]);
|
|
@@ -214,55 +200,12 @@ async function equal(given, expected) {
|
|
|
214
200
|
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
215
201
|
return diff === 0;
|
|
216
202
|
}
|
|
217
|
-
var CALL_TTL_MS = 2 * 36e5;
|
|
218
203
|
var randomToken = () => btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
219
204
|
var b64 = (text) => btoa(String.fromCharCode(...encode(text))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
220
205
|
var unb64 = (text) => new TextDecoder().decode(Uint8Array.from(atob(text.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)));
|
|
221
206
|
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
222
207
|
var json = (body, status = 200) => Response.json(body, { status });
|
|
223
208
|
var describe = (error) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error)).replace(/sk-tan-[\w-]+|Bearer\s+\S+/g, "<redacted>").slice(0, 400);
|
|
224
|
-
function normalizeAddress(value) {
|
|
225
|
-
const trimmed = value.trim();
|
|
226
|
-
if (trimmed.includes("@")) return trimmed.toLowerCase();
|
|
227
|
-
const digits = trimmed.replace(/[^\d]/g, "");
|
|
228
|
-
if (digits.length < 8 || digits.length > 15) return null;
|
|
229
|
-
return `+${digits.length === 10 && !trimmed.startsWith("+") ? `1${digits}` : digits}`;
|
|
230
|
-
}
|
|
231
|
-
function debugCommand(text) {
|
|
232
|
-
const match = /^debug\s+(on|off)$/i.exec(text.trim());
|
|
233
|
-
return match ? match[1].toLowerCase() === "on" : null;
|
|
234
|
-
}
|
|
235
|
-
var num = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
236
|
-
var seconds = (ms) => ms < 1e4 ? `${(Math.max(0, ms) / 1e3).toFixed(1)}s` : `${Math.round(ms / 1e3)}s`;
|
|
237
|
-
var count = (n) => n < 1e3 ? String(n) : n < 1e4 ? `${(n / 1e3).toFixed(1)}k` : `${Math.round(n / 1e3)}k`;
|
|
238
|
-
function debugFooter(agent, turnId, trace, totalMs) {
|
|
239
|
-
const parts = [agent];
|
|
240
|
-
if (trace.harness) parts.push(trace.harness);
|
|
241
|
-
if (trace.model) parts.push(trace.model.split("/").at(-1));
|
|
242
|
-
if (trace.box) parts.push(trace.box);
|
|
243
|
-
if (totalMs !== void 0) parts.push(`${seconds(totalMs)}${trace.runMs !== void 0 ? ` (run ${seconds(trace.runMs)})` : ""}`);
|
|
244
|
-
else if (trace.runMs !== void 0) parts.push(`run ${seconds(trace.runMs)}`);
|
|
245
|
-
if (trace.inputTokens !== void 0 && trace.outputTokens !== void 0) {
|
|
246
|
-
parts.push(`${count(trace.inputTokens)}\u2192${count(trace.outputTokens)} tok${trace.reasoningTokens ? ` (${count(trace.reasoningTokens)} thinking)` : ""}`);
|
|
247
|
-
}
|
|
248
|
-
if (trace.costUsd !== void 0 && (trace.costUsd > 0 || !trace.outputTokens)) {
|
|
249
|
-
parts.push(trace.costUsd >= 0.01 ? `$${trace.costUsd.toFixed(3)}` : `$${trace.costUsd.toPrecision(2)}`);
|
|
250
|
-
}
|
|
251
|
-
if (trace.tools !== void 0) parts.push(`${trace.tools} tool${trace.tools === 1 ? "" : "s"}`);
|
|
252
|
-
parts.push(turnId.slice(0, 8));
|
|
253
|
-
return `\u2699 ${parts.join(" \xB7 ")}`;
|
|
254
|
-
}
|
|
255
|
-
function noteResult(trace, result, usage) {
|
|
256
|
-
const reported = result.tokenUsage ?? result.usage ?? {};
|
|
257
|
-
const timing = result.timing ?? {};
|
|
258
|
-
const started = num(timing.startedAt), completed = num(timing.completedAt);
|
|
259
|
-
if (started !== void 0 && completed !== void 0) trace.runMs = completed - started;
|
|
260
|
-
trace.inputTokens = num(reported.inputTokens) ?? num(usage?.inputTokens);
|
|
261
|
-
trace.outputTokens = num(reported.outputTokens) ?? num(usage?.outputTokens);
|
|
262
|
-
trace.reasoningTokens = num(reported.reasoningTokens);
|
|
263
|
-
if (result.usdKnown !== false) trace.costUsd = num(reported.cost) ?? num(result.costUsd) ?? num(result.totalCostUsd);
|
|
264
|
-
if (Array.isArray(result.toolInvocations)) trace.tools = result.toolInvocations.length;
|
|
265
|
-
}
|
|
266
209
|
var HostedAgentError = class extends Error {
|
|
267
210
|
constructor(code, message) {
|
|
268
211
|
super(message);
|
|
@@ -272,35 +215,25 @@ var HostedAgentError = class extends Error {
|
|
|
272
215
|
code;
|
|
273
216
|
};
|
|
274
217
|
function createHostedAgent(config) {
|
|
275
|
-
const store = config.store;
|
|
276
218
|
const policy = { ...DEFAULT_BOX_POLICY, ...config.box };
|
|
277
219
|
const wallCapMs = config.turnWallCapMs ?? 12e4;
|
|
278
220
|
const sandbox = new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? "https://sandbox.tangle.tools", timeoutMs: 2e4 });
|
|
279
|
-
if (!(
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
221
|
+
if (!E164.test(config.owner)) throw new HostedAgentError("owner_not_e164", "owner must be an E.164 phone number, such as +15550100001.");
|
|
222
|
+
const backend = { ...config.harness ? { type: config.harness } : {}, profile: conversationProfile(config.profile) };
|
|
223
|
+
const create = {
|
|
224
|
+
name: "hosted-person",
|
|
225
|
+
resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },
|
|
226
|
+
egressPolicy: { mode: "strict", allowDomains: policy.allowDomains, includeImplicitDomains: false },
|
|
227
|
+
idleTimeoutSeconds: policy.idleTimeoutSeconds,
|
|
228
|
+
maxLifetimeSeconds: policy.maxLifetimeSeconds,
|
|
229
|
+
deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds
|
|
230
|
+
};
|
|
231
|
+
async function personBox(phone, deadline) {
|
|
286
232
|
let box;
|
|
287
233
|
try {
|
|
288
234
|
({ box } = await sandbox.instances.ensure({
|
|
289
|
-
key:
|
|
290
|
-
|
|
291
|
-
// A box this kit made before the platform kept instances keeps its files.
|
|
292
|
-
adopt: await store.get(`box:${user}`) ?? void 0,
|
|
293
|
-
create: {
|
|
294
|
-
name: `hosted-${user.slice(0, 24)}`,
|
|
295
|
-
resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },
|
|
296
|
-
secrets: [],
|
|
297
|
-
sshEnabled: false,
|
|
298
|
-
egressPolicy: { mode: "strict", allowDomains: policy.allowDomains, includeImplicitDomains: false },
|
|
299
|
-
idleTimeoutSeconds: policy.idleTimeoutSeconds,
|
|
300
|
-
maxLifetimeSeconds: policy.maxLifetimeSeconds,
|
|
301
|
-
deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds,
|
|
302
|
-
metadata: { hostedUser: user }
|
|
303
|
-
}
|
|
235
|
+
key: await lineInstanceKey(PERSON_KEY_PREFIX, phone),
|
|
236
|
+
create: { ...create, secrets: [], sshEnabled: false }
|
|
304
237
|
}));
|
|
305
238
|
} catch (error) {
|
|
306
239
|
if (error instanceof InstanceRestartingError) throw new TurnPending("box_restarting", true);
|
|
@@ -316,105 +249,35 @@ function createHostedAgent(config) {
|
|
|
316
249
|
throw error;
|
|
317
250
|
}
|
|
318
251
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
free: { turnsPerDay: config.freeTurnsPerDay ?? 20, usdPerDay: null },
|
|
326
|
-
paid: null,
|
|
327
|
-
spendResourceType: null
|
|
328
|
-
});
|
|
329
|
-
})().catch((error) => {
|
|
330
|
-
planned = void 0;
|
|
331
|
-
throw error;
|
|
332
|
-
});
|
|
333
|
-
async function admit(message, user) {
|
|
334
|
-
const stopKey = `stop:${user}`;
|
|
335
|
-
if (message.channel !== "voice" && STOP.test(message.text.trim())) {
|
|
336
|
-
await store.put(stopKey, "1");
|
|
337
|
-
return { reply: NOTICE.stopped };
|
|
338
|
-
}
|
|
339
|
-
if (message.channel !== "voice" && START.test(message.text.trim())) {
|
|
340
|
-
await store.put(stopKey, "");
|
|
341
|
-
return { reply: NOTICE.started };
|
|
252
|
+
async function callSession(phone) {
|
|
253
|
+
for (const line of await sandbox.lines.list()) {
|
|
254
|
+
if (line.attachment?.instance?.keyPrefix !== PERSON_KEY_PREFIX) continue;
|
|
255
|
+
const member = (await sandbox.lines.members(line.id).list()).find((m) => m.address === phone);
|
|
256
|
+
const thread = member && (await sandbox.lines.threads(line.id).list()).find((t) => t.memberId === member.id);
|
|
257
|
+
if (thread) return thread.sessionId;
|
|
342
258
|
}
|
|
343
|
-
|
|
344
|
-
await ensurePlan();
|
|
345
|
-
const allowance = await hub.allowances.admit(meter, {
|
|
346
|
-
member: user,
|
|
347
|
-
role: message.userId === owner ? "owner" : "member",
|
|
348
|
-
turnId: message.turnId,
|
|
349
|
-
channel: message.channel
|
|
350
|
-
});
|
|
351
|
-
if (allowance.decision === "admit") return "answer";
|
|
352
|
-
return allowance.decision === "paywall" && config.allow ? await config.allow(message, allowance.turns.used) : { reply: NOTICE.limit };
|
|
259
|
+
return `voice-${(await sha256(phone)).slice(0, 32)}`;
|
|
353
260
|
}
|
|
354
|
-
async function
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const sessionId = `hosted-${user}-${await profileTag}`;
|
|
359
|
-
const trace = options.trace;
|
|
360
|
-
const conversationKey = `convo:${user}`;
|
|
361
|
-
let declined;
|
|
362
|
-
let conversation, at = "";
|
|
363
|
-
const outcome = await runHostedTurn({ turnId: message.turnId, text }, {
|
|
364
|
-
// The allowance counts a turn id once per person and admits its retries
|
|
365
|
-
// again, so a model-supplied voice ticket cannot spend another person's turn.
|
|
366
|
-
async admit() {
|
|
367
|
-
const decision = await admit(message, user);
|
|
368
|
-
if (decision === "answer") return null;
|
|
369
|
-
declined = decision === "ignore" ? { state: "declined" } : { state: "declined", reply: decision.reply };
|
|
370
|
-
return { ok: false, reason: "refused", detail: "declined" };
|
|
371
|
-
},
|
|
261
|
+
async function answerCall(call, text, turnId, deadline) {
|
|
262
|
+
const outcome = await runHostedTurn({ turnId, text }, {
|
|
263
|
+
// Calls are admitted by `voiceHook`; Hub counts texts.
|
|
264
|
+
admit: async () => null,
|
|
372
265
|
async box() {
|
|
373
|
-
const box = await
|
|
266
|
+
const box = await personBox(call.phone, deadline);
|
|
374
267
|
if (!box) throw new TurnPending("box_starting", true);
|
|
375
268
|
return box;
|
|
376
269
|
},
|
|
377
|
-
sessionId,
|
|
270
|
+
sessionId: call.session,
|
|
378
271
|
backend: async () => backend,
|
|
379
|
-
async
|
|
380
|
-
|
|
381
|
-
${text}` : text;
|
|
382
|
-
at = `${box.id}/${sessionId}`;
|
|
383
|
-
const stored = await store.get(conversationKey);
|
|
384
|
-
conversation = stored ? JSON.parse(stored) : { at, turns: [] };
|
|
385
|
-
const carried = conversation.at === at ? "" : conversation.turns.map(([, them, you]) => `Them: ${them}
|
|
386
|
-
You: ${you}`).join("\n");
|
|
387
|
-
return carried ? `${CARRY_HEAD}
|
|
388
|
-
${carried}
|
|
389
|
-
${CARRY_NOW}
|
|
390
|
-
${asked}` : asked;
|
|
391
|
-
},
|
|
392
|
-
async answered(answer) {
|
|
393
|
-
const turns = (conversation?.turns ?? []).filter(([id]) => id !== message.turnId);
|
|
394
|
-
turns.push([message.turnId, message.channel === "voice" ? `(on a call) ${text}` : text, answer]);
|
|
395
|
-
while (turns.length > 1 && JSON.stringify(turns).length > CARRY_CHARS) turns.shift();
|
|
396
|
-
await store.put(conversationKey, JSON.stringify({ at, turns }));
|
|
397
|
-
},
|
|
272
|
+
prompt: async () => `[Phone call. Answer in one to three short spoken sentences.]
|
|
273
|
+
${text}`,
|
|
398
274
|
classify: (error) => error instanceof HostedAgentError ? { ok: false, reason: "unavailable", detail: error.code } : void 0,
|
|
399
275
|
observe: {
|
|
400
|
-
readCreatedSession: Boolean(trace),
|
|
401
|
-
span(step, ms) {
|
|
402
|
-
if (trace && step === "ensure") trace.box ??= `box ${seconds(ms)}`;
|
|
403
|
-
},
|
|
404
|
-
session(info) {
|
|
405
|
-
if (!trace) return;
|
|
406
|
-
trace.harness = info.harness ?? config.harness;
|
|
407
|
-
trace.model = info.model;
|
|
408
|
-
},
|
|
409
|
-
drive(result) {
|
|
410
|
-
if (trace && result.state === "completed") noteResult(trace, result.result, result.usage);
|
|
411
|
-
},
|
|
412
276
|
failure(step, failure) {
|
|
413
|
-
console.error(`[hosted-agent] turn=${
|
|
277
|
+
console.error(`[hosted-agent] voice turn=${turnId} step=${step} code=${failure.code} message=${JSON.stringify(failure.message)}`);
|
|
414
278
|
}
|
|
415
279
|
}
|
|
416
|
-
}, { wallCapMs, timeoutMs: 8e3, until:
|
|
417
|
-
if (declined) return declined;
|
|
280
|
+
}, { wallCapMs, timeoutMs: 8e3, until: deadline });
|
|
418
281
|
if (outcome.ok === true) return { state: "answered", text: outcome.text };
|
|
419
282
|
if (outcome.ok === "pending") {
|
|
420
283
|
if (outcome.cause !== void 0) throw outcome.cause;
|
|
@@ -423,80 +286,31 @@ ${asked}` : asked;
|
|
|
423
286
|
const code = outcome.detail ?? outcome.reason;
|
|
424
287
|
throw new HostedAgentError(code === "agent_failed" ? "turn_failed" : code, `The turn ended without an answer (${code}).`);
|
|
425
288
|
}
|
|
289
|
+
function voiceStore() {
|
|
290
|
+
if (!config.store) throw new HostedAgentError("store_missing", "voice needs a store for call tokens");
|
|
291
|
+
return config.store;
|
|
292
|
+
}
|
|
426
293
|
return {
|
|
427
|
-
ask,
|
|
428
294
|
/**
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
295
|
+
* Attach an Inkbox iMessage identity, connected to Hub under the
|
|
296
|
+
* developer's account, as this agent's line: the owner and anyone who
|
|
297
|
+
* texts it each get their own box and thread. Safe to repeat with the
|
|
298
|
+
* same config. Remove any Hub event subscription on the connection first;
|
|
299
|
+
* Hub refuses a line that another route would also answer.
|
|
432
300
|
*/
|
|
433
|
-
async
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
301
|
+
async attachLine(connectionId) {
|
|
302
|
+
const line = await sandbox.lines.fromConnection({ connectionId, transport: "imessage", clientReference: "hosted-agent" });
|
|
303
|
+
return sandbox.lines.attach({
|
|
304
|
+
number: line.id,
|
|
305
|
+
mode: "shared",
|
|
306
|
+
members: [{ address: config.owner, role: "owner" }],
|
|
307
|
+
unknownSenders: "guest",
|
|
308
|
+
roles: { owner: PERSON, guest: PERSON },
|
|
309
|
+
respond: { kind: "agent", backend },
|
|
310
|
+
limits: { turnsPerMemberPerDay: config.freeTurnsPerDay ?? 20 },
|
|
311
|
+
instance: { keyPrefix: PERSON_KEY_PREFIX, create },
|
|
312
|
+
clientReference: "hosted-agent"
|
|
443
313
|
});
|
|
444
|
-
return subscription;
|
|
445
|
-
},
|
|
446
|
-
/** The Hub callback. Authenticates within Hub's 10 s budget and runs nothing. */
|
|
447
|
-
async receive(request, secret) {
|
|
448
|
-
const authenticated = await authenticateHubEventRequest({ request, secret, maxBodyBytes: 64 * 1024 });
|
|
449
|
-
if (!authenticated.ok) return { response: authenticated.response };
|
|
450
|
-
const { runId, source, providerEvent } = authenticated.delivery;
|
|
451
|
-
const ack = new Response(null, { status: 204 });
|
|
452
|
-
if (source.kind !== "connection" || source.event !== IMESSAGE_EVENT || providerEvent.connectionId !== source.id) return { response: ack };
|
|
453
|
-
return { response: ack, inbound: { runId, connectionId: source.id, event: providerEvent } };
|
|
454
|
-
},
|
|
455
|
-
/**
|
|
456
|
-
* Answer one inbound message and send the reply through Hub. Returns
|
|
457
|
-
* 'pending' while the turn still runs; call again with the same inbound.
|
|
458
|
-
* On `lastAttempt`, a failure is answered with an apology instead of thrown.
|
|
459
|
-
*/
|
|
460
|
-
async respond(inbound, options = {}) {
|
|
461
|
-
const normalized = normalizeConversationEvent(inbound.event);
|
|
462
|
-
if (!normalized.ok) return "ignored";
|
|
463
|
-
const event = normalized.event;
|
|
464
|
-
const userId = normalizeAddress(event.sender.id ?? "");
|
|
465
|
-
if (!userId || !event.text?.trim() || event.isGroup || event.historyOnly) return "ignored";
|
|
466
|
-
const turnId = `t-${(await sha256(inbound.runId)).slice(0, 40)}`;
|
|
467
|
-
const isOwner = Boolean(config.owner) && normalizeAddress(config.owner) === userId;
|
|
468
|
-
const debugKey = `debug:${(await sha256(userId)).slice(0, 32)}`;
|
|
469
|
-
const toggle = isOwner ? debugCommand(event.text) : null;
|
|
470
|
-
const trace = isOwner && toggle === null && await store.get(debugKey) ? {} : void 0;
|
|
471
|
-
let text;
|
|
472
|
-
try {
|
|
473
|
-
if (toggle !== null) {
|
|
474
|
-
await store.put(debugKey, toggle ? "1" : "");
|
|
475
|
-
text = toggle ? NOTICE.debugOn : NOTICE.debugOff;
|
|
476
|
-
} else {
|
|
477
|
-
const result = await ask(
|
|
478
|
-
{ userId, channel: "imessage", text: event.text, turnId },
|
|
479
|
-
{ deadline: Date.now() + wallCapMs + 6e4, trace }
|
|
480
|
-
);
|
|
481
|
-
if (result.state === "pending") return "pending";
|
|
482
|
-
if (result.state === "declined" && !result.reply) return "ignored";
|
|
483
|
-
text = result.state === "answered" ? result.text : result.reply;
|
|
484
|
-
if (trace && result.state === "answered") {
|
|
485
|
-
const totalMs = event.occurredAt !== null && event.occurredAt !== void 0 ? Date.now() - event.occurredAt : void 0;
|
|
486
|
-
text = `${text.slice(0, 1300)}
|
|
487
|
-
|
|
488
|
-
${debugFooter(config.profile.name ?? "agent", turnId, trace, totalMs)}`;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
} catch (error) {
|
|
492
|
-
if (!(error instanceof HostedAgentError) && !options.lastAttempt) throw error;
|
|
493
|
-
console.error(`[hosted-agent] turn=${turnId} ${describe(error)}`);
|
|
494
|
-
text = NOTICE.unavailable;
|
|
495
|
-
}
|
|
496
|
-
const plan = buildMessagingReply(inbound.event, text.slice(0, 1500), `reply-${turnId}`);
|
|
497
|
-
if (!plan.ok) return "ignored";
|
|
498
|
-
await hub.tools.invoke(plan.reply.action, plan.reply.input, { connectionId: inbound.connectionId, idempotencyKey: plan.reply.idempotencyKey });
|
|
499
|
-
return "replied";
|
|
500
314
|
},
|
|
501
315
|
/**
|
|
502
316
|
* ph0ny's call hook. `admit` returns a call token bound to the caller's
|
|
@@ -507,11 +321,14 @@ ${debugFooter(config.profile.name ?? "agent", turnId, trace, totalMs)}`;
|
|
|
507
321
|
if (!secret || !await equal(request.headers.get("authorization"), `Bearer ${secret}`)) return json({ error: "unauthorized" }, 401);
|
|
508
322
|
const body = await request.json().catch(() => null);
|
|
509
323
|
if (body?.event !== "admit") return json({ ok: true });
|
|
510
|
-
const phone =
|
|
511
|
-
if (!phone
|
|
512
|
-
if (await store.get(`stop:${(await sha256(phone)).slice(0, 32)}`)) return json({ admit: false, say: "You unsubscribed from this line." });
|
|
324
|
+
const phone = body.phone?.trim() ?? "";
|
|
325
|
+
if (!E164.test(phone)) return json({ admit: false, say: "Sorry, I can only take calls from a visible number." });
|
|
513
326
|
const callToken = randomToken();
|
|
514
|
-
await
|
|
327
|
+
await voiceStore().put(
|
|
328
|
+
`vcall:${callToken}`,
|
|
329
|
+
JSON.stringify({ phone, exp: Date.now() + CALL_TTL_MS }),
|
|
330
|
+
{ expirationTtl: CALL_TTL_MS / 1e3 }
|
|
331
|
+
);
|
|
515
332
|
return json({ admit: true, callToken });
|
|
516
333
|
},
|
|
517
334
|
/**
|
|
@@ -522,6 +339,7 @@ ${debugFooter(config.profile.name ?? "agent", turnId, trace, totalMs)}`;
|
|
|
522
339
|
const secret = config.voiceSecret;
|
|
523
340
|
if (!secret || !await equal(request.headers.get("authorization"), `Bearer ${secret}`)) return json({ error: "unauthorized" }, 401);
|
|
524
341
|
const token = request.headers.get("x-voice-call-token") ?? "";
|
|
342
|
+
const store = voiceStore();
|
|
525
343
|
const admitted = /^[A-Za-z0-9_-]{16,128}$/.test(token) ? await store.get(`vcall:${token}`) : null;
|
|
526
344
|
if (!admitted) return json({ status: "error", error: "call_not_admitted" }, 403);
|
|
527
345
|
const call = JSON.parse(admitted);
|
|
@@ -530,9 +348,13 @@ ${debugFooter(config.profile.name ?? "agent", turnId, trace, totalMs)}`;
|
|
|
530
348
|
const [turnId, asked] = body?.ticket ? body.ticket.split(".") : [`v-${crypto.randomUUID()}`, b64(body?.utterance ?? "")];
|
|
531
349
|
if (!turnId || !asked) return json({ status: "error", error: "missing_utterance" }, 400);
|
|
532
350
|
try {
|
|
351
|
+
if (!call.session) {
|
|
352
|
+
call.session = await callSession(call.phone);
|
|
353
|
+
await store.put(`vcall:${token}`, JSON.stringify(call), { expirationTtl: Math.max(60, Math.ceil((call.exp - Date.now()) / 1e3)) });
|
|
354
|
+
}
|
|
533
355
|
const budgetMs = config.voiceBudgetMs ?? 8e3;
|
|
534
356
|
const result = await Promise.race([
|
|
535
|
-
|
|
357
|
+
answerCall(call, unb64(asked), turnId, Date.now() + budgetMs),
|
|
536
358
|
sleep(budgetMs).then(() => ({ state: "pending" }))
|
|
537
359
|
]);
|
|
538
360
|
if (result.state === "pending") return json({ status: "pending", ticket: `${turnId}.${asked}` });
|
|
@@ -553,15 +375,12 @@ export {
|
|
|
553
375
|
HostedAgentError,
|
|
554
376
|
MAX_MESSAGE_CHARS,
|
|
555
377
|
MAX_REPEATED_FAILURES,
|
|
556
|
-
|
|
378
|
+
PERSON_KEY_PREFIX,
|
|
557
379
|
TurnPending,
|
|
558
380
|
chatOnlyBackend,
|
|
559
381
|
createHostedAgent,
|
|
560
|
-
debugCommand,
|
|
561
|
-
debugFooter,
|
|
562
382
|
describeFailure,
|
|
563
383
|
isTransientFailure,
|
|
564
|
-
normalizeAddress,
|
|
565
384
|
runHostedTurn,
|
|
566
385
|
settleHostedTurn
|
|
567
386
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hosted-agent/index.ts","../../src/hosted-agent/engine.ts"],"sourcesContent":["import { authenticateHubEventRequest, HubClient, type HubProviderEvent } from '@tangle-network/hub-sdk'\nimport { buildMessagingReply, normalizeConversationEvent } from '@tangle-network/agent-integrations/conversation-events'\nimport type { AgentProfile, BackendConfig, SandboxInstance } from '@tangle-network/sandbox'\nimport { InstanceRestartingError, Sandbox } from '@tangle-network/sandbox/core'\nimport { runHostedTurn, TurnPending } from './engine'\n\nexport * from './engine'\n\n/**\n * A hosted agent: a person texts or calls a line, and the agent answers from\n * that person's own isolated sandbox. The developer's Tangle API key pays for\n * every box, model turn and reply.\n *\n * The platform keeps each person's box and counts their turns: each person\n * is one Sandbox instance (`sandbox.instances`), a fresh isolated box built\n * from the published profile, and one member of a Hub allowance meter.\n * Later messages resume the same box and conversation, so text and voice\n * share one memory. A new profile starts a new session in the same box; a\n * replaced box starts a new one. Either way the person's recent conversation\n * carries over from the store.\n *\n * Every step is idempotent by the message's turn id: a retried delivery\n * settles the same sandbox turn, the same allowance admission and the same\n * Hub send. So the caller may retry any failure, and needs no dedupe table.\n */\n\n/** Durable key-value storage for STOP state, the debug switch, call tokens and recent turns. A Cloudflare KV namespace satisfies it. */\nexport interface HostedAgentStore {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n}\n\nexport type HostedChannel = 'imessage' | 'voice'\n\nexport interface HostedMessage {\n /** The sender's address as the channel delivered it: E.164 for a phone. */\n userId: string\n channel: HostedChannel\n text: string\n /** Stable per logical message. Reusing it settles the same turn. */\n turnId: string\n}\n\n/** What the allowance hook decides for one new message. */\nexport type Allowance = 'answer' | 'ignore' | { reply: string }\n\nexport interface HostedAgentConfig {\n /** The developer's Tangle API key (Sandbox and Hub). */\n apiKey: string\n /**\n * The persona every person's box runs. A profile without `model.default`\n * runs {@link DEFAULT_HOSTED_MODEL}, and a profile without `tools` runs with\n * {@link CONVERSATION_TOOLS_OFF} turned off. Set `tools` to choose your own,\n * for example on a harness that cannot turn those tools off.\n */\n profile: AgentProfile\n /** Backend harness type, such as `opencode`; the runtime default when omitted. */\n harness?: string\n store: HostedAgentStore\n /**\n * The Platform meter that counts this agent's turns (Hub\n * `hub.allowances`). Each person is one member. Default `hosted-agent`.\n */\n meter?: string\n /**\n * Answers per person per UTC day before `allow` must decide. Default 20.\n * Written to the meter's plan when the meter has none; a plan already set\n * on the Platform wins.\n */\n freeTurnsPerDay?: number\n /**\n * Decide a new message past the free allowance when paying would lift the\n * limit (the Platform's `paywall` decision), for example reply with a\n * checkout link. Without it, or past the paid allowance, a person is told to\n * come back tomorrow. Retries of an admitted message never reach this hook.\n */\n allow?: (message: HostedMessage, usedToday: number) => Allowance | Promise<Allowance>\n /**\n * The owner's own address (E.164 phone). Only this sender can text DEBUG ON\n * or DEBUG OFF; while on, each reply to them ends with one ⚙ line naming the\n * harness, model, box, time, tokens, cost and tools of that turn.\n */\n owner?: string\n /** Shared secret for the ph0ny call hook and `ask_workspace` webhook tool. */\n voiceSecret?: string\n /** How long one `ask_workspace` call may wait before it returns a ticket. Default 8 s. */\n voiceBudgetMs?: number\n box?: Partial<BoxPolicy>\n turnWallCapMs?: number\n sandboxUrl?: string\n hubUrl?: string\n}\n\nexport interface BoxPolicy {\n cpuCores: number\n memoryMB: number\n diskGB: number\n idleTimeoutSeconds: number\n maxLifetimeSeconds: number\n deleteAfterStoppedSeconds: number\n /** Egress allow-list. The default reaches the model router only. */\n allowDomains: string[]\n}\n\nexport const DEFAULT_BOX_POLICY: BoxPolicy = {\n cpuCores: 1, memoryMB: 2048, diskGB: 10,\n idleTimeoutSeconds: 600, maxLifetimeSeconds: 86_400, deleteAfterStoppedSeconds: 7 * 86_400,\n allowDomains: ['router.tangle.tools'],\n}\n\n/**\n * Harness tools a texting or calling assistant does not use. Their\n * descriptions present every turn as coding work: a shell, file search,\n * sub-agents, to-do lists, skills and web fetch. File read, write and edit\n * stay, so a persona can keep notes such as `memory.md`.\n */\nexport const CONVERSATION_TOOLS_OFF = ['bash', 'glob', 'grep', 'task', 'todowrite', 'webfetch', 'skill'] as const\n\n/**\n * The model for a profile without `model.default`. It gave the most useful\n * on-topic replies among four Router models on the same five texts and calls\n * (2026-09-23), within the latency of the others.\n */\nexport const DEFAULT_HOSTED_MODEL = 'openai/gpt-5.6-luna'\n\n/** The profile a person's box runs: the developer's profile over the conversation defaults. */\nfunction conversationProfile(profile: AgentProfile): AgentProfile {\n return {\n ...profile,\n model: { ...profile.model, default: profile.model?.default ?? DEFAULT_HOSTED_MODEL },\n // A profile that sets `tools` owns its tool set.\n ...(profile.tools ? {} : {\n tools: Object.fromEntries(CONVERSATION_TOOLS_OFF.map(tool => [tool, false])),\n // The sandbox's preview policy grants the shell unless its permission\n // is denied, so turning the tool off alone leaves the shell in place.\n permissions: { bash: 'deny' as const, ...profile.permissions },\n }),\n }\n}\n\n/** What one turn reports about itself, for the owner's debug line. A figure\n * the run did not report stays undefined and is left out of the line. */\nexport interface TurnTrace {\n /** Time the platform took to hand over the person's running box, such as `box 0.3s`. */\n box?: string\n harness?: string\n model?: string\n /** Sandbox clock: the run's own start to finish. */\n runMs?: number\n inputTokens?: number\n outputTokens?: number\n reasoningTokens?: number\n costUsd?: number\n tools?: number\n}\n\nexport type AskResult =\n | { state: 'answered'; text: string }\n | { state: 'declined'; reply?: string }\n | { state: 'pending' }\n\n/** A Hub delivery that passed authentication, in a form a queue can carry. */\nexport interface HostedInbound {\n runId: string\n connectionId: string\n event: HubProviderEvent\n}\n\nexport const NOTICE = {\n debugOn: 'Debug on. Each reply now ends with a ⚙ line: agent, harness, model, box, time, tokens, cost, tools and turn. Text DEBUG OFF to stop.',\n debugOff: 'Debug off. Replies no longer carry the ⚙ line.',\n stopped: 'You are unsubscribed. Text START to talk again.',\n started: 'Welcome back. Text me anytime. Reply STOP to stop.',\n limit: 'You have used today\\'s free messages. Talk tomorrow!',\n unavailable: 'Sorry, I could not answer that just now. Please try again in a minute.',\n} as const\n\nconst IMESSAGE_EVENT = 'inkbox.imessage.received'\nconst IMESSAGE_REPLY = 'inkbox.imessage.reply'\nconst STOP = /^(stop|stopall|unsubscribe|cancel|end|quit)$/i\nconst START = /^(start|unstop|resume)$/i\n/** How much of a person's recent conversation a new session inherits. */\nconst CARRY_CHARS = 6000\nconst CARRY_HEAD = '[Your conversation with this person so far. Your instructions may have changed since; follow the current ones.]'\nconst CARRY_NOW = '[Their new message:]'\n\n/** A person's recent turns, kept outside the box so a new session or box can continue them. */\ninterface Conversation {\n /** The box and session the turns last ran in. */\n at: string\n /** Oldest first: turn id, what they said, what the agent answered. */\n turns: Array<[string, string, string]>\n}\n\nconst encode = (text: string) => new TextEncoder().encode(text)\nconst hex = (bytes: ArrayBuffer) => Array.from(new Uint8Array(bytes), byte => byte.toString(16).padStart(2, '0')).join('')\nconst sha256 = async (value: string) => hex(await crypto.subtle.digest('SHA-256', encode(value)))\n/** Compares digests, so the time taken reveals nothing about the secret. */\nasync function equal(given: string | null | undefined, expected: string): Promise<boolean> {\n if (!given) return false\n const [a, b] = await Promise.all([sha256(given), sha256(expected)])\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\nconst CALL_TTL_MS = 2 * 3_600_000\n/** 43 url-safe characters from 32 random bytes. */\nconst randomToken = () => btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\nconst b64 = (text: string) => btoa(String.fromCharCode(...encode(text))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\nconst unb64 = (text: string) => new TextDecoder().decode(Uint8Array.from(atob(text.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)))\nconst sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))\nconst json = (body: unknown, status = 200) => Response.json(body, { status })\nconst describe = (error: unknown) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error))\n .replace(/sk-tan-[\\w-]+|Bearer\\s+\\S+/g, '<redacted>').slice(0, 400)\n\n/** One address per person: E.164 for anything phone-shaped, else lowercase. */\nexport function normalizeAddress(value: string): string | null {\n const trimmed = value.trim()\n if (trimmed.includes('@')) return trimmed.toLowerCase()\n const digits = trimmed.replace(/[^\\d]/g, '')\n if (digits.length < 8 || digits.length > 15) return null\n return `+${digits.length === 10 && !trimmed.startsWith('+') ? `1${digits}` : digits}`\n}\n\n/** `true` for DEBUG ON, `false` for DEBUG OFF, `null` for any other text. */\nexport function debugCommand(text: string): boolean | null {\n const match = /^debug\\s+(on|off)$/i.exec(text.trim())\n return match ? match[1]!.toLowerCase() === 'on' : null\n}\n\nconst num = (value: unknown) => typeof value === 'number' && Number.isFinite(value) ? value : undefined\nconst seconds = (ms: number) => ms < 10_000 ? `${(Math.max(0, ms) / 1000).toFixed(1)}s` : `${Math.round(ms / 1000)}s`\nconst count = (n: number) => n < 1000 ? String(n) : n < 10_000 ? `${(n / 1000).toFixed(1)}k` : `${Math.round(n / 1000)}k`\n\n/** The owner's ⚙ line. `totalMs` runs from the provider's receipt of the text to now. */\nexport function debugFooter(agent: string, turnId: string, trace: TurnTrace, totalMs?: number): string {\n const parts = [agent]\n if (trace.harness) parts.push(trace.harness)\n if (trace.model) parts.push(trace.model.split('/').at(-1)!)\n if (trace.box) parts.push(trace.box)\n if (totalMs !== undefined) parts.push(`${seconds(totalMs)}${trace.runMs !== undefined ? ` (run ${seconds(trace.runMs)})` : ''}`)\n else if (trace.runMs !== undefined) parts.push(`run ${seconds(trace.runMs)}`)\n if (trace.inputTokens !== undefined && trace.outputTokens !== undefined) {\n parts.push(`${count(trace.inputTokens)}→${count(trace.outputTokens)} tok${trace.reasoningTokens ? ` (${count(trace.reasoningTokens)} thinking)` : ''}`)\n }\n // Zero on a turn that used tokens means the harness had no price for the model.\n if (trace.costUsd !== undefined && (trace.costUsd > 0 || !trace.outputTokens)) {\n parts.push(trace.costUsd >= 0.01 ? `$${trace.costUsd.toFixed(3)}` : `$${trace.costUsd.toPrecision(2)}`)\n }\n if (trace.tools !== undefined) parts.push(`${trace.tools} tool${trace.tools === 1 ? '' : 's'}`)\n parts.push(turnId.slice(0, 8))\n return `⚙ ${parts.join(' · ')}`\n}\n\n/** Model, tokens, cost, tools and run time from a finished turn, as far as it reports them. */\nfunction noteResult(trace: TurnTrace, result: Record<string, unknown>, usage?: { inputTokens?: number; outputTokens?: number }) {\n // A completed turn arrives as the run's own record (`tokenUsage`, `timing`,\n // `toolInvocations`) or as the platform's cached summary (`usage`, `costUsd`).\n // Both count every prompt token; the SDK's typed usage counts only the\n // uncached tail on the summary (3 of 1,214), so it is the fallback. The\n // summary's `durationMs` is not the run time, so that shape has none.\n const reported = (result.tokenUsage ?? result.usage ?? {}) as Record<string, unknown>\n const timing = (result.timing ?? {}) as Record<string, unknown>\n const started = num(timing.startedAt), completed = num(timing.completedAt)\n if (started !== undefined && completed !== undefined) trace.runMs = completed - started\n trace.inputTokens = num(reported.inputTokens) ?? num(usage?.inputTokens)\n trace.outputTokens = num(reported.outputTokens) ?? num(usage?.outputTokens)\n trace.reasoningTokens = num(reported.reasoningTokens)\n if (result.usdKnown !== false) trace.costUsd = num(reported.cost) ?? num(result.costUsd) ?? num(result.totalCostUsd)\n if (Array.isArray(result.toolInvocations)) trace.tools = result.toolInvocations.length\n}\n\nexport class HostedAgentError extends Error {\n constructor(readonly code: string, message: string) { super(message); this.name = 'HostedAgentError' }\n}\n\nexport function createHostedAgent(config: HostedAgentConfig) {\n const store = config.store\n const policy = { ...DEFAULT_BOX_POLICY, ...config.box }\n const wallCapMs = config.turnWallCapMs ?? 120_000\n const sandbox = new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? 'https://sandbox.tangle.tools', timeoutMs: 20_000 })\n // The peer range admits older Sandbox minors for other subpaths; named instances arrived in 0.50.\n if (!('instances' in sandbox)) throw new HostedAgentError('sandbox_too_old', 'hosted-agent needs @tangle-network/sandbox 0.50 or later.')\n const hub = new HubClient({ baseUrl: config.hubUrl ?? 'https://id.tangle.tools', apiKey: config.apiKey })\n // The Hub SDK peer range admits older minors for apps that do not host agents; the allowance meter arrived in 0.17.\n if (!('allowances' in hub)) throw new HostedAgentError('hub_sdk_too_old', 'hosted-agent needs @tangle-network/hub-sdk 0.17 or later.')\n const profile = conversationProfile(config.profile)\n const backend: BackendConfig = { ...(config.harness ? { type: config.harness as BackendConfig['type'] } : {}), profile }\n const profileTag = sha256(JSON.stringify(profile)).then(hash => hash.slice(0, 8))\n\n /** The person's running box, kept by the platform. Null when the deadline passed while it starts. */\n async function ensureBox(user: string, deadline: number): Promise<SandboxInstance | null> {\n let box: SandboxInstance\n try {\n ({ box } = await sandbox.instances.ensure({\n key: `hosted:${user}`,\n profile: { version: await profileTag, backend },\n // A box this kit made before the platform kept instances keeps its files.\n adopt: await store.get(`box:${user}`) ?? undefined,\n create: {\n name: `hosted-${user.slice(0, 24)}`,\n resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },\n secrets: [], sshEnabled: false,\n egressPolicy: { mode: 'strict', allowDomains: policy.allowDomains, includeImplicitDomains: false },\n idleTimeoutSeconds: policy.idleTimeoutSeconds, maxLifetimeSeconds: policy.maxLifetimeSeconds,\n deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds, metadata: { hostedUser: user },\n },\n }))\n } catch (error) {\n // The platform replaces a box that keeps failing to start; until then the turn waits.\n if (error instanceof InstanceRestartingError) throw new TurnPending('box_restarting', true)\n throw error\n }\n if (box.status === 'running') return box\n const timeoutMs = deadline - Date.now()\n if (timeoutMs < 1000) return null\n try {\n return await sandbox.waitForRunning(box.id, { timeoutMs })\n } catch (error) {\n if (error instanceof Error && error.name === 'TimeoutError') return null\n throw error\n }\n }\n\n const meter = config.meter ?? 'hosted-agent'\n const owner = config.owner ? normalizeAddress(config.owner) : null\n /** Give the meter the configured allowance once, unless it already has a plan. */\n let planned: Promise<void> | undefined\n const ensurePlan = () => planned ??= (async () => {\n if ((await hub.allowances.plan(meter)).configured) return\n await hub.allowances.setPlan(meter, { free: { turnsPerDay: config.freeTurnsPerDay ?? 20, usdPerDay: null }, paid: null,\n spendResourceType: null })\n })().catch(error => { planned = undefined; throw error })\n\n /** Admit a new message once: STOP/START, then the Platform's allowance, then `allow`. */\n async function admit(message: HostedMessage, user: string): Promise<Allowance> {\n const stopKey = `stop:${user}`\n if (message.channel !== 'voice' && STOP.test(message.text.trim())) { await store.put(stopKey, '1'); return { reply: NOTICE.stopped } }\n if (message.channel !== 'voice' && START.test(message.text.trim())) { await store.put(stopKey, ''); return { reply: NOTICE.started } }\n if (await store.get(stopKey)) return 'ignore'\n await ensurePlan()\n // The Platform counts atomically and never counts one turn id twice.\n const allowance = await hub.allowances.admit(meter, { member: user, role: message.userId === owner ? 'owner' : 'member',\n turnId: message.turnId, channel: message.channel })\n if (allowance.decision === 'admit') return 'answer'\n // Past the paid allowance, nothing the person buys lifts today's limit.\n return allowance.decision === 'paywall' && config.allow ? await config.allow(message, allowance.turns.used) : { reply: NOTICE.limit }\n }\n\n /** Run one message in the person's box until it answers or `deadline`. */\n async function ask(message: HostedMessage, options: { deadline: number; trace?: TurnTrace }): Promise<AskResult> {\n const text = message.text.trim()\n if (!text || text.length > 8000 || !/^[\\w-]{1,120}$/.test(message.turnId)) return { state: 'declined' }\n const user = (await sha256(message.userId)).slice(0, 32)\n // A session binds its backend when created, and the runtime would give a\n // bare session its generic default assistant. A new profile starts a new\n // session, so the persona always matches the one in force.\n const sessionId = `hosted-${user}-${await profileTag}`\n const trace = options.trace\n const conversationKey = `convo:${user}`\n let declined: AskResult | undefined\n let conversation: Conversation | undefined, at = ''\n const outcome = await runHostedTurn({ turnId: message.turnId, text }, {\n // The allowance counts a turn id once per person and admits its retries\n // again, so a model-supplied voice ticket cannot spend another person's turn.\n async admit() {\n const decision = await admit(message, user)\n if (decision === 'answer') return null\n declined = decision === 'ignore' ? { state: 'declined' } : { state: 'declined', reply: decision.reply }\n return { ok: false, reason: 'refused', detail: 'declined' }\n },\n async box() {\n const box = await ensureBox(user, options.deadline)\n if (!box) throw new TurnPending('box_starting', true)\n return box\n },\n sessionId,\n backend: async () => backend,\n async prompt(box) {\n const asked = message.channel === 'voice' ? `[Phone call. Answer in one to three short spoken sentences.]\\n${text}` : text\n // The first turn in a new session or box inherits the person's recent\n // turns. A retry before that turn completes carries them again, so a\n // failed first attempt loses nothing.\n at = `${box.id}/${sessionId}`\n const stored = await store.get(conversationKey)\n conversation = stored ? JSON.parse(stored) as Conversation : { at, turns: [] }\n const carried = conversation.at === at ? '' : conversation.turns.map(([, them, you]) => `Them: ${them}\\nYou: ${you}`).join('\\n')\n return carried ? `${CARRY_HEAD}\\n${carried}\\n${CARRY_NOW}\\n${asked}` : asked\n },\n async answered(answer) {\n // Concurrent turns may drop one another's entry; the box keeps the full record.\n const turns = (conversation?.turns ?? []).filter(([id]) => id !== message.turnId)\n turns.push([message.turnId, message.channel === 'voice' ? `(on a call) ${text}` : text, answer])\n while (turns.length > 1 && JSON.stringify(turns).length > CARRY_CHARS) turns.shift()\n await store.put(conversationKey, JSON.stringify({ at, turns } satisfies Conversation))\n },\n classify: error => error instanceof HostedAgentError ? { ok: false, reason: 'unavailable', detail: error.code } : undefined,\n observe: {\n readCreatedSession: Boolean(trace),\n span(step, ms) {\n if (trace && step === 'ensure') trace.box ??= `box ${seconds(ms)}`\n },\n session(info) {\n if (!trace) return\n trace.harness = info.harness ?? config.harness\n trace.model = info.model\n },\n drive(result) {\n if (trace && result.state === 'completed') noteResult(trace, result.result, result.usage)\n },\n failure(step, failure) {\n console.error(`[hosted-agent] turn=${message.turnId} step=${step} code=${failure.code} message=${JSON.stringify(failure.message)}`)\n },\n },\n }, { wallCapMs, timeoutMs: 8000, until: options.deadline })\n if (declined) return declined\n if (outcome.ok === true) return { state: 'answered', text: outcome.text }\n // A thrown failure is rethrown, so the caller's retry policy decides it.\n if (outcome.ok === 'pending') {\n if (outcome.cause !== undefined) throw outcome.cause\n return { state: 'pending' }\n }\n const code = outcome.detail ?? outcome.reason\n throw new HostedAgentError(code === 'agent_failed' ? 'turn_failed' : code, `The turn ended without an answer (${code}).`)\n }\n\n return {\n ask,\n\n /**\n * One-time setup: route an Inkbox iMessage identity's messages, connected\n * to Hub under the developer's account, to `callbackUrl`, and allow the\n * one reply action on that connection. Safe to repeat.\n */\n async connect(input: { connectionId: string; identityId: string; callbackUrl: string; secret: string }) {\n const webhook = await hub.connections.inboundWebhook.get(input.connectionId)\n if (!webhook.configured) await hub.connections.inboundWebhook.create(input.connectionId, { identityId: input.identityId, imessage: true })\n await hub.permissions.set({ connectionId: input.connectionId, actionPath: IMESSAGE_REPLY, decision: 'allow' })\n const { subscription } = await hub.eventSubscriptions.create({\n clientReference: `hosted-agent:${input.connectionId}`, label: 'Hosted agent messages',\n source: { type: 'connection', connectionId: input.connectionId }, event: IMESSAGE_EVENT,\n callback: { url: input.callbackUrl, secret: input.secret },\n })\n return subscription\n },\n\n /** The Hub callback. Authenticates within Hub's 10 s budget and runs nothing. */\n async receive(request: Request, secret: string): Promise<{ response: Response; inbound?: HostedInbound }> {\n const authenticated = await authenticateHubEventRequest({ request, secret, maxBodyBytes: 64 * 1024 })\n if (!authenticated.ok) return { response: authenticated.response }\n const { runId, source, providerEvent } = authenticated.delivery\n const ack = new Response(null, { status: 204 })\n if (source.kind !== 'connection' || source.event !== IMESSAGE_EVENT || providerEvent.connectionId !== source.id) return { response: ack }\n return { response: ack, inbound: { runId, connectionId: source.id, event: providerEvent } }\n },\n\n /**\n * Answer one inbound message and send the reply through Hub. Returns\n * 'pending' while the turn still runs; call again with the same inbound.\n * On `lastAttempt`, a failure is answered with an apology instead of thrown.\n */\n async respond(inbound: HostedInbound, options: { lastAttempt?: boolean } = {}): Promise<'replied' | 'ignored' | 'pending'> {\n const normalized = normalizeConversationEvent(inbound.event)\n if (!normalized.ok) return 'ignored'\n const event = normalized.event\n const userId = normalizeAddress(event.sender.id ?? '')\n if (!userId || !event.text?.trim() || event.isGroup || event.historyOnly) return 'ignored'\n const turnId = `t-${(await sha256(inbound.runId)).slice(0, 40)}`\n // Only the configured owner toggles debug; anyone else's DEBUG ON is an ordinary message.\n const isOwner = Boolean(config.owner) && normalizeAddress(config.owner!) === userId\n const debugKey = `debug:${(await sha256(userId)).slice(0, 32)}`\n const toggle = isOwner ? debugCommand(event.text) : null\n const trace: TurnTrace | undefined = isOwner && toggle === null && await store.get(debugKey) ? {} : undefined\n let text: string\n try {\n if (toggle !== null) {\n await store.put(debugKey, toggle ? '1' : '')\n text = toggle ? NOTICE.debugOn : NOTICE.debugOff\n } else {\n const result = await ask({ userId, channel: 'imessage', text: event.text, turnId },\n { deadline: Date.now() + wallCapMs + 60_000, trace })\n if (result.state === 'pending') return 'pending'\n if (result.state === 'declined' && !result.reply) return 'ignored'\n text = result.state === 'answered' ? result.text : result.reply!\n if (trace && result.state === 'answered') {\n const totalMs = event.occurredAt !== null && event.occurredAt !== undefined ? Date.now() - event.occurredAt : undefined\n text = `${text.slice(0, 1300)}\\n\\n${debugFooter(config.profile.name ?? 'agent', turnId, trace, totalMs)}`\n }\n }\n } catch (error) {\n // A failed turn fails the same way on every retry; anything else may clear.\n if (!(error instanceof HostedAgentError) && !options.lastAttempt) throw error\n console.error(`[hosted-agent] turn=${turnId} ${describe(error)}`)\n text = NOTICE.unavailable\n }\n const plan = buildMessagingReply(inbound.event, text.slice(0, 1500), `reply-${turnId}`)\n if (!plan.ok) return 'ignored'\n // Hub replays the first result for a retry with this key and input.\n await hub.tools.invoke(plan.reply.action, plan.reply.input, { connectionId: inbound.connectionId, idempotencyKey: plan.reply.idempotencyKey })\n return 'replied'\n },\n\n /**\n * ph0ny's call hook. `admit` returns a call token bound to the caller's\n * number from the carrier's signed call record, never from the model.\n */\n async voiceHook(request: Request): Promise<Response> {\n const secret = config.voiceSecret\n if (!secret || !await equal(request.headers.get('authorization'), `Bearer ${secret}`)) return json({ error: 'unauthorized' }, 401)\n const body = await request.json().catch(() => null) as { event?: string; phone?: string } | null\n if (body?.event !== 'admit') return json({ ok: true })\n const phone = normalizeAddress(body.phone ?? '')\n if (!phone?.startsWith('+')) return json({ admit: false, say: 'Sorry, I can only take calls from a visible number.' })\n if (await store.get(`stop:${(await sha256(phone)).slice(0, 32)}`)) return json({ admit: false, say: 'You unsubscribed from this line.' })\n // ph0ny accepts only an opaque token matching ^[A-Za-z0-9_-]{16,128}$,\n // so the caller's number stays here and the token is its random key.\n const callToken = randomToken()\n await store.put(`vcall:${callToken}`, JSON.stringify({ phone, exp: Date.now() + CALL_TTL_MS }), { expirationTtl: CALL_TTL_MS / 1000 })\n return json({ admit: true, callToken })\n },\n\n /**\n * ph0ny's `ask_workspace` webhook tool, configured with `forwardCallToken`.\n * A slow turn returns a ticket; the voice agent asks again with it.\n */\n async voiceAsk(request: Request): Promise<Response> {\n const secret = config.voiceSecret\n if (!secret || !await equal(request.headers.get('authorization'), `Bearer ${secret}`)) return json({ error: 'unauthorized' }, 401)\n const token = request.headers.get('x-voice-call-token') ?? ''\n const admitted = /^[A-Za-z0-9_-]{16,128}$/.test(token) ? await store.get(`vcall:${token}`) : null\n if (!admitted) return json({ status: 'error', error: 'call_not_admitted' }, 403)\n const call = JSON.parse(admitted) as { phone: string; exp: number }\n if (call.exp < Date.now()) return json({ status: 'error', error: 'call_expired' }, 403)\n const body = await request.json().catch(() => null) as { utterance?: string; ticket?: string } | null\n // A ticket carries its own turn id and question, so a retry after a\n // slow first step still admits and settles exactly one turn.\n const [turnId, asked] = body?.ticket ? body.ticket.split('.') : [`v-${crypto.randomUUID()}`, b64(body?.utterance ?? '')]\n if (!turnId || !asked) return json({ status: 'error', error: 'missing_utterance' }, 400)\n try {\n // ph0ny drops the tool call at its own timeout, and resuming an idle\n // box can take longer than the budget. Answer 'pending' at the\n // budget regardless; every step is idempotent by turn id, so the\n // ticket call continues where this one stopped.\n const budgetMs = config.voiceBudgetMs ?? 8000\n const result = await Promise.race([\n ask({ userId: call.phone, channel: 'voice', text: unb64(asked), turnId }, { deadline: Date.now() + budgetMs }),\n sleep(budgetMs).then((): AskResult => ({ state: 'pending' })),\n ])\n if (result.state === 'pending') return json({ status: 'pending', ticket: `${turnId}.${asked}` })\n if (result.state === 'declined') return json({ status: 'complete', answer: result.reply ?? 'I cannot answer that right now.' })\n return json({ status: 'complete', answer: result.text })\n } catch (error) {\n console.error(`[hosted-agent] voice turn=${turnId} ${describe(error)}`)\n return json({ status: 'error', error: 'agent_unavailable' }, 502)\n }\n },\n }\n}\n\nexport type HostedAgent = ReturnType<typeof createHostedAgent>\n","import type { BackendConfig, CreateSessionOptions } from '@tangle-network/sandbox'\nimport type { ConversationTurnOptions, ConversationTurnResult, PromptInputPart } from '@tangle-network/sandbox/core'\n\n/**\n * The one turn engine for a hosted agent, whatever hosts it. This kit's\n * `createHostedAgent` runs on it with a key-value store, and Agent Builder\n * runs on it with its D1 enrollments, per-user keys, spaces and product\n * ingest. A host supplies those as ports; the engine owns the order of the\n * steps and what each outcome and failure means.\n *\n * One call is one non-blocking pass for one message:\n *\n * validate -> admit once -> the person's box -> authorize -> session -> drive -> classify\n *\n * Every step is idempotent by the turn id: the sandbox's admission receipt\n * settles a repeated drive on the same turn, so a host may repeat a pass for\n * the same message any number of times and the agent runs it once.\n */\n\nexport type TurnRefusal = 'not_enrolled' | 'execution_not_ready' | 'refused' | 'unavailable'\nexport type TurnReply = { ok: true; text: string } | { ok: false; reason: TurnRefusal; detail?: string }\nexport type TurnStep = 'ensure' | 'drive' | 'turn'\n/**\n * Where an unfinished turn stands. `code` is the same on every repeat of the\n * same cause, so a host can tell a stuck turn from a slow one. A transient\n * cause (a lost response, a timeout, a busy platform, a box still starting)\n * is expected to clear by itself; any other repeats until it is fixed.\n */\nexport interface PendingDetail { step: TurnStep; code: string; transient: boolean }\n/** `cause` is what a port or the Sandbox threw, when a throw left the turn pending. */\nexport type TurnOutcome = TurnReply | { ok: 'pending'; detail: PendingDetail; cause?: unknown }\n\nexport const MAX_MESSAGE_CHARS = 8000\n/** Identical non-transient failures in a row after which a host fails a turn. */\nexport const MAX_REPEATED_FAILURES = 3\n/** Refusal detail when the host's `authorize` check fails. */\nexport const AUTHORITY_ENDED = 'authority_ended'\n\n/** The part of a Sandbox box one turn uses. `SandboxInstance` satisfies it. */\nexport interface TurnBox {\n readonly id: string\n driveConversationTurn(message: string | PromptInputPart[], options: ConversationTurnOptions): Promise<ConversationTurnResult>\n session(id: string): { status(): Promise<unknown> }\n createSession(options: CreateSessionOptions): Promise<unknown>\n}\n\n/** A port throws this to report a turn that is not ready yet, such as a box still starting. */\nexport class TurnPending extends Error {\n constructor(readonly code: string, readonly transient: boolean) {\n super(code)\n this.name = 'TurnPending'\n }\n}\n\n/** A thrown value as a stable code plus a secret-free message for a log line. */\nexport interface TurnFailure { name: string; code: string; status?: number; reason?: string; message: string }\n\n/** What a pass reports while it runs. Every hook is optional; none changes the outcome. */\nexport interface TurnObserver {\n /** Time one step of this pass took, whether it returned or threw. */\n span?(step: 'ensure' | 'session' | 'drive', ms: number): void\n /** The session the turn runs in, as the runtime reports it, or the backend it was created with. */\n session?(info: { harness?: string; model?: string; created: boolean }): void\n /**\n * Read a session this pass created once more, so `session` reports the\n * harness and model the runtime chose for it. It costs one request, so a\n * host asks for it only when it shows them (an owner's debug line, say).\n */\n readCreatedSession?: boolean\n /** Each drive result and the time it arrived. */\n drive?(result: ConversationTurnResult, at: number): void\n /** A failure that left the turn pending or refused. */\n failure?(step: TurnStep, failure: TurnFailure, boxId?: string): void | Promise<void>\n}\n\n/** Storage and policy a host supplies for one message. */\nexport interface HostedTurnPorts<Box extends TurnBox = TurnBox> {\n /**\n * Admit this turn once, before any compute. Return null to run it, or the\n * refusal. A turn admitted by an earlier pass must return null again, so a\n * retry never consumes allowance twice.\n */\n admit(): Promise<Extract<TurnReply, { ok: false }> | null>\n /** The person's running box: created, resumed or replaced as needed. Throw to report why not. */\n box(): Promise<Box>\n /** The host's own authority (a phone link, say), read after the box is ready and right before the drive. */\n authorize?(): Promise<boolean>\n /** The session this turn runs in. A session binds its backend when created, so a new persona needs a new id. */\n sessionId: string\n /** The backend a missing session is created with. Without it the runtime answers as its generic assistant. */\n backend(): Promise<BackendConfig>\n /** The message as the agent receives it: text, or text and images. */\n prompt(box: Box): Promise<string | PromptInputPart[]>\n /** Runs once the turn has a non-empty answer, before the pass returns it. */\n answered?(answer: string, box: Box): Promise<void>\n /** Map a thrown value to an outcome. Undefined leaves it to the engine's own classification. */\n classify?(error: unknown, step: 'ensure' | 'drive'): TurnOutcome | undefined\n observe?: TurnObserver\n}\n\nexport interface HostedTurnOptions {\n /** The run's wall-clock cap; the Sandbox cancels a turn that exceeds it. */\n wallCapMs: number\n /** How long one drive request may wait on a running turn. Default 8 s. */\n timeoutMs?: number\n /** False only reads a turn admitted earlier; it never admits or starts one. */\n allowDispatch?: boolean\n /** Keep driving a running turn until this time, instead of returning pending after one drive. */\n until?: number\n /** Wait between drives while `until` allows. Default 2 s. */\n pollMs?: number\n now?: () => number\n sleep?: (ms: number) => Promise<void>\n}\n\n/** Harness and model as the runtime reports them; the model it selected after normalization wins. */\nfunction sessionInfo(session: unknown): { harness?: string; model?: string } {\n const s = (typeof session === 'object' && session !== null ? session : {}) as\n { backend?: unknown; model?: unknown; effectiveBackend?: { model?: unknown } | null }\n const effective = s.effectiveBackend?.model\n const model = typeof effective === 'string' && effective ? effective : typeof s.model === 'string' && s.model ? s.model : undefined\n return { ...(typeof s.backend === 'string' && s.backend ? { harness: s.backend } : {}), ...(model ? { model } : {}) }\n}\n\nconst refusal = (reason: TurnRefusal, detail?: string): Extract<TurnReply, { ok: false }> => ({ ok: false, reason, ...(detail ? { detail } : {}) })\nconst RUNNING = new Set(['running', 'awaiting_input', 'not_admitted'])\nconst TRANSIENT_ERRORS = new Set(['NetworkError', 'TimeoutError', 'QuotaError', 'AbortError', 'EdgeNotReadyError'])\n/**\n * The 503 codes the Sandbox SDK itself classifies as refused before dispatch\n * and safe to retry. A 503 with any other code, such as\n * HUB_CREDENTIAL_REFRESH_FAILED, has not shown it will clear by itself.\n */\nconst TRANSIENT_503 = new Set(['MODEL_CREDENTIAL_SUPERSEDED', 'HUB_CREDENTIAL_REFRESH_UNAVAILABLE'])\n\n/** The code omits the message, which can carry ids or times that differ on every repeat of one cause. */\nexport function describeFailure(error: unknown): TurnFailure {\n const e = (typeof error === 'object' && error !== null ? error : {}) as Record<string, unknown>\n const name = typeof e.name === 'string' ? e.name : typeof error\n const status = typeof e.status === 'number' ? e.status : undefined\n const own = typeof e.code === 'string' ? e.code : typeof e.reason === 'string' ? e.reason : undefined\n const code = [name, own, status].filter(part => part !== undefined).join(':').replace(/[^\\w.:-]/g, '_').slice(0, 100)\n const message = String(typeof e.message === 'string' ? e.message : error)\n .replace(/sk-tan-[\\w-]+|eyJ[\\w-]+\\.[\\w-]+\\.[\\w-]*|Bearer\\s+\\S+/g, '<redacted>').slice(0, 400)\n return { name, code, ...(status !== undefined ? { status } : {}), ...(own !== undefined ? { reason: own } : {}), message }\n}\n\nexport function isTransientFailure(failure: Pick<TurnFailure, 'name' | 'status' | 'reason'>): boolean {\n return TRANSIENT_ERRORS.has(failure.name) || [408, 429, 502, 504].includes(failure.status ?? 0) ||\n (failure.status === 503 && (failure.reason === undefined || TRANSIENT_503.has(failure.reason)))\n}\n\n/**\n * The platform refuses a funded request once the paying key's budget is\n * spent. That refusal is decided, so retrying it would loop forever.\n */\nconst budgetExhausted = (error: unknown) => error instanceof Error && error.name === 'KeyBudgetExhaustedError'\n\n/** One non-blocking pass for one message. Safe to repeat with the same turn id. */\nexport async function runHostedTurn<Box extends TurnBox>(turn: { turnId: string; text: string }, ports: HostedTurnPorts<Box>,\n options: HostedTurnOptions): Promise<TurnOutcome> {\n const now = options.now ?? Date.now\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const observe = ports.observe ?? {}\n const allowDispatch = options.allowDispatch !== false\n const text = turn.text.trim()\n if (!text || text.length > MAX_MESSAGE_CHARS || !turn.turnId || turn.turnId.length > 200) return refusal('refused', 'invalid_message')\n const refused = await ports.admit()\n if (refused) return refused\n\n const timed = async <T>(step: 'ensure' | 'session' | 'drive', fn: () => Promise<T>): Promise<T> => {\n const start = now()\n try { return await fn() } finally { observe.span?.(step, now() - start) }\n }\n const failed = async (step: 'ensure' | 'drive', error: unknown, boxId?: string): Promise<TurnOutcome> => {\n const failure = describeFailure(error)\n await observe.failure?.(step, failure, boxId)\n const classified = ports.classify?.(error, step)\n if (classified) return classified\n if (error instanceof TurnPending) return { ok: 'pending', detail: { step, code: error.code, transient: error.transient } }\n if (budgetExhausted(error)) return refusal('refused', 'budget_exhausted')\n // An admission whose response was lost is retried with the same turn id;\n // the runtime's admission receipt prevents a second execution.\n return { ok: 'pending', detail: { step, code: failure.code, transient: isTransientFailure(failure) }, cause: error }\n }\n\n let box: Box\n try {\n box = await timed('ensure', () => ports.box())\n } catch (error) {\n return failed('ensure', error)\n }\n if (ports.authorize && !await ports.authorize()) return refusal('refused', AUTHORITY_ENDED)\n\n let result: ConversationTurnResult\n try {\n const prompt = await ports.prompt(box)\n const sessionId = ports.sessionId\n // The SDK would create a missing session with no profile, and the runtime\n // would then answer as its generic default assistant.\n if (allowDispatch) await timed('session', async () => {\n const session = await box.session(sessionId).status()\n if (session) {\n observe.session?.({ created: false, ...sessionInfo(session) })\n return\n }\n const backend = await ports.backend()\n await box.createSession({ sessionId, retention: 'workspace', backend })\n // A new session names its harness and model only once the runtime has created it.\n const created = observe.session && observe.readCreatedSession ? sessionInfo(await box.session(sessionId).status()) : {}\n observe.session?.({ created: true, ...(backend.type ? { harness: backend.type } : {}), ...created })\n })\n for (;;) {\n result = await timed('drive', () => box.driveConversationTurn(prompt, { sessionId, turnId: turn.turnId,\n wallCapMs: options.wallCapMs, timeoutMs: options.timeoutMs ?? 8000, allowDispatch }))\n observe.drive?.(result, now())\n const pollMs = options.pollMs ?? 2000\n if (!RUNNING.has(result.state) || options.until === undefined || now() + pollMs > options.until) break\n await sleep(pollMs)\n }\n } catch (error) {\n return failed('drive', error, box.id)\n }\n switch (result.state) {\n case 'completed': {\n const answer = result.text.trim()\n if (!answer) {\n await observe.failure?.('turn', describeFailure(new Error('The turn completed with no reply text')), box.id)\n return refusal('unavailable', 'empty_reply')\n }\n await ports.answered?.(answer, box)\n return { ok: true, text: answer }\n }\n case 'failed':\n await observe.failure?.('turn', describeFailure(Object.assign(new Error(result.error), { name: 'TurnFailed' })), box.id)\n return refusal('unavailable', 'agent_failed')\n // Interaction and plan approval are not exposed over messaging. The wall\n // cap cancels a turn that waits on one, which frees the session.\n // Sandbox 0.45 reported approval and question outcomes as `failed`.\n case 'awaiting_plan_decision': case 'blocked_on_approval': case 'awaiting_question': case 'awaiting_interaction':\n return refusal('refused', 'needs_decision')\n case 'running': case 'awaiting_input': case 'not_admitted':\n return { ok: 'pending', detail: { step: 'turn', code: result.state, transient: true } }\n }\n}\n\n/**\n * Repeat passes until the turn settles, for a caller that holds a connection\n * open (a voice question, say). A turn the runtime never finishes must not\n * hold the caller forever, so `deadline` bounds the wait; `signal` stops the\n * wait only, and the admitted turn keeps running.\n */\nexport async function settleHostedTurn(pass: () => Promise<TurnOutcome>, options: {\n deadline: number\n pollMs?: number\n signal?: AbortSignal\n now?: () => number\n sleep?: (ms: number, signal?: AbortSignal) => Promise<void>\n}): Promise<TurnReply> {\n const now = options.now ?? Date.now\n const sleep = options.sleep ?? ((ms: number, signal?: AbortSignal) => new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason) }, { once: true })\n }))\n let last = '', repeats = 0\n for (;;) {\n if (options.signal?.aborted || now() > options.deadline) return refusal('unavailable', 'pending')\n const outcome = await pass()\n if (outcome.ok !== 'pending') return outcome\n repeats = outcome.detail.code === last ? repeats + 1 : 1\n last = outcome.detail.code\n if (!outcome.detail.transient && repeats >= MAX_REPEATED_FAILURES) return refusal('unavailable', `${outcome.detail.step}:${last}`)\n try { await sleep(options.pollMs ?? 1500, options.signal) } catch { return refusal('unavailable', 'pending') }\n }\n}\n\n/**\n * A space member who may chat only: no shell and no file writes in the shared\n * box. The profile's other settings are kept.\n */\nexport function chatOnlyBackend(backend: BackendConfig): BackendConfig {\n const profile = backend.profile ?? {}\n return { ...backend, profile: { ...profile,\n tools: { ...profile.tools, bash: false, edit: false, write: false },\n permissions: { ...profile.permissions, bash: 'deny', edit: 'deny' } } }\n}\n"],"mappings":"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","names":["sleep"]}
|
|
1
|
+
{"version":3,"sources":["../../src/hosted-agent/index.ts","../../src/hosted-agent/engine.ts"],"sourcesContent":["import type { AgentProfile, BackendConfig, SandboxInstance } from '@tangle-network/sandbox'\nimport { InstanceRestartingError, type LineAttachment, type LineInstanceCreate, lineInstanceKey, Sandbox } from '@tangle-network/sandbox/core'\nimport { runHostedTurn, TurnPending } from './engine'\n\nexport * from './engine'\n\n/**\n * A hosted agent: people text or call a line, and each person is answered\n * from their own isolated sandbox. The developer's Tangle API key pays for\n * every box, model turn and reply.\n *\n * The platform does the work. {@link HostedAgent.attachLine} attaches the\n * line to Tangle Hub with one sandbox per person: Hub routes each text to the\n * sender's box (the named instance {@link PERSON_KEY_PREFIX} plus a hash of\n * their number), keeps one thread per person, handles STOP and START, counts\n * each person's texts per day, and sends the reply. A call through ph0ny runs\n * in the same box and the same thread, so text and voice share one memory.\n */\n\n/** Durable key-value storage for voice call tokens. A Cloudflare KV namespace satisfies it. */\nexport interface HostedAgentStore {\n get(key: string): Promise<string | null>\n put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>\n}\n\nexport interface HostedAgentConfig {\n /** The developer's Tangle API key. */\n apiKey: string\n /**\n * The persona every person's box runs. A profile without `model.default`\n * runs {@link DEFAULT_HOSTED_MODEL}, and a profile without `tools` runs with\n * {@link CONVERSATION_TOOLS_OFF} turned off. Set `tools` to choose your own,\n * for example on a harness that cannot turn those tools off.\n */\n profile: AgentProfile\n /** Backend harness type, such as `opencode`; the runtime default when omitted. */\n harness?: string\n /** The owner's own phone (E.164), the line's first member. */\n owner: string\n /** Texts Hub answers per person per UTC day. Default 20. */\n freeTurnsPerDay?: number\n box?: Partial<BoxPolicy>\n /** Voice call tokens. Required for `voiceHook` and `voiceAsk`. */\n store?: HostedAgentStore\n /** Shared secret for the ph0ny call hook and `ask_workspace` webhook tool. */\n voiceSecret?: string\n /** How long one `ask_workspace` call may wait before it returns a ticket. Default 8 s. */\n voiceBudgetMs?: number\n turnWallCapMs?: number\n sandboxUrl?: string\n}\n\nexport interface BoxPolicy {\n cpuCores: number\n memoryMB: number\n diskGB: number\n idleTimeoutSeconds: number\n maxLifetimeSeconds: number\n deleteAfterStoppedSeconds: number\n /** Egress allow-list. The default reaches the model router only. */\n allowDomains: string[]\n}\n\nexport const DEFAULT_BOX_POLICY: BoxPolicy = {\n cpuCores: 1, memoryMB: 2048, diskGB: 10,\n idleTimeoutSeconds: 600, maxLifetimeSeconds: 86_400, deleteAfterStoppedSeconds: 7 * 86_400,\n allowDomains: ['router.tangle.tools'],\n}\n\n/**\n * Each person's box is the developer's named instance with this prefix. It is\n * the key this kit used before Hub routed its texts, so every existing\n * person keeps their box.\n */\nexport const PERSON_KEY_PREFIX = 'hosted:'\n\n/**\n * Harness tools a texting or calling assistant does not use. Their\n * descriptions present every turn as coding work: a shell, file search,\n * sub-agents, to-do lists, skills and web fetch. File read, write and edit\n * stay, so a persona can keep notes such as `memory.md`.\n */\nexport const CONVERSATION_TOOLS_OFF = ['bash', 'glob', 'grep', 'task', 'todowrite', 'webfetch', 'skill'] as const\n\n/**\n * The model for a profile without `model.default`. It gave the most useful\n * on-topic replies among four Router models on the same five texts and calls\n * (2026-09-23), within the latency of the others.\n */\nexport const DEFAULT_HOSTED_MODEL = 'openai/gpt-5.6-luna'\n\n/** The profile a person's box runs: the developer's profile over the conversation defaults. */\nfunction conversationProfile(profile: AgentProfile): AgentProfile {\n return {\n ...profile,\n model: { ...profile.model, default: profile.model?.default ?? DEFAULT_HOSTED_MODEL },\n // A profile that sets `tools` owns its tool set.\n ...(profile.tools ? {} : {\n tools: Object.fromEntries(CONVERSATION_TOOLS_OFF.map(tool => [tool, false])),\n // The sandbox's preview policy grants the shell unless its permission\n // is denied, so turning the tool off alone leaves the shell in place.\n permissions: { bash: 'deny' as const, ...profile.permissions },\n }),\n }\n}\n\nexport type AskResult =\n | { state: 'answered'; text: string }\n | { state: 'declined'; reply?: string }\n | { state: 'pending' }\n\n/** Each person runs in their own box, so no member shares a disk and each may use the persona's tools. */\nconst PERSON = { context: 'own', tools: 'act' } as const\nconst CALL_TTL_MS = 2 * 3_600_000\nconst E164 = /^\\+[1-9]\\d{6,14}$/\nconst encode = (text: string) => new TextEncoder().encode(text)\nconst sha256 = async (value: string) =>\n Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encode(value))), byte => byte.toString(16).padStart(2, '0')).join('')\n/** Compares digests, so the time taken reveals nothing about the secret. */\nasync function equal(given: string | null | undefined, expected: string): Promise<boolean> {\n if (!given) return false\n const [a, b] = await Promise.all([sha256(given), sha256(expected)])\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return diff === 0\n}\n/** 43 url-safe characters from 32 random bytes. */\nconst randomToken = () => btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(32)))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\nconst b64 = (text: string) => btoa(String.fromCharCode(...encode(text))).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\nconst unb64 = (text: string) => new TextDecoder().decode(Uint8Array.from(atob(text.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)))\nconst sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))\nconst json = (body: unknown, status = 200) => Response.json(body, { status })\nconst describe = (error: unknown) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error))\n .replace(/sk-tan-[\\w-]+|Bearer\\s+\\S+/g, '<redacted>').slice(0, 400)\n\nexport class HostedAgentError extends Error {\n constructor(readonly code: string, message: string) { super(message); this.name = 'HostedAgentError' }\n}\n\n/** A call admitted by `voiceHook`: the caller's number and, once found, their text thread's session. */\ninterface Call { phone: string; exp: number; session?: string }\n\nexport function createHostedAgent(config: HostedAgentConfig) {\n const policy = { ...DEFAULT_BOX_POLICY, ...config.box }\n const wallCapMs = config.turnWallCapMs ?? 120_000\n const sandbox = new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? 'https://sandbox.tangle.tools', timeoutMs: 20_000 })\n if (!E164.test(config.owner)) throw new HostedAgentError('owner_not_e164', 'owner must be an E.164 phone number, such as +15550100001.')\n const backend: BackendConfig = { ...(config.harness ? { type: config.harness as BackendConfig['type'] } : {}), profile: conversationProfile(config.profile) }\n const create: LineInstanceCreate = {\n name: 'hosted-person',\n resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },\n egressPolicy: { mode: 'strict', allowDomains: policy.allowDomains, includeImplicitDomains: false },\n idleTimeoutSeconds: policy.idleTimeoutSeconds, maxLifetimeSeconds: policy.maxLifetimeSeconds,\n deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds,\n }\n\n /** The caller's running box: the instance Hub runs their texts in. Null when the deadline passed while it starts. */\n async function personBox(phone: string, deadline: number): Promise<SandboxInstance | null> {\n let box: SandboxInstance\n try {\n ({ box } = await sandbox.instances.ensure({ key: await lineInstanceKey(PERSON_KEY_PREFIX, phone),\n create: { ...create, secrets: [], sshEnabled: false } }))\n } catch (error) {\n // The platform replaces a box that keeps failing to start; until then the turn waits.\n if (error instanceof InstanceRestartingError) throw new TurnPending('box_restarting', true)\n throw error\n }\n if (box.status === 'running') return box\n const timeoutMs = deadline - Date.now()\n if (timeoutMs < 1000) return null\n try {\n return await sandbox.waitForRunning(box.id, { timeoutMs })\n } catch (error) {\n if (error instanceof Error && error.name === 'TimeoutError') return null\n throw error\n }\n }\n\n /** The session of the caller's text thread, so a call continues the conversation; a caller who never texted gets their own. */\n async function callSession(phone: string): Promise<string> {\n for (const line of await sandbox.lines.list()) {\n if (line.attachment?.instance?.keyPrefix !== PERSON_KEY_PREFIX) continue\n const member = (await sandbox.lines.members(line.id).list()).find(m => m.address === phone)\n const thread = member && (await sandbox.lines.threads(line.id).list()).find(t => t.memberId === member.id)\n if (thread) return thread.sessionId\n }\n return `voice-${(await sha256(phone)).slice(0, 32)}`\n }\n\n /** Answer one spoken question in the caller's box until it answers or `deadline`. */\n async function answerCall(call: Call, text: string, turnId: string, deadline: number): Promise<AskResult> {\n const outcome = await runHostedTurn({ turnId, text }, {\n // Calls are admitted by `voiceHook`; Hub counts texts.\n admit: async () => null,\n async box() {\n const box = await personBox(call.phone, deadline)\n if (!box) throw new TurnPending('box_starting', true)\n return box\n },\n sessionId: call.session!,\n backend: async () => backend,\n prompt: async () => `[Phone call. Answer in one to three short spoken sentences.]\\n${text}`,\n classify: error => error instanceof HostedAgentError ? { ok: false, reason: 'unavailable', detail: error.code } : undefined,\n observe: {\n failure(step, failure) {\n console.error(`[hosted-agent] voice turn=${turnId} step=${step} code=${failure.code} message=${JSON.stringify(failure.message)}`)\n },\n },\n }, { wallCapMs, timeoutMs: 8000, until: deadline })\n if (outcome.ok === true) return { state: 'answered', text: outcome.text }\n // A thrown failure is rethrown, so the caller's retry policy decides it.\n if (outcome.ok === 'pending') {\n if (outcome.cause !== undefined) throw outcome.cause\n return { state: 'pending' }\n }\n const code = outcome.detail ?? outcome.reason\n throw new HostedAgentError(code === 'agent_failed' ? 'turn_failed' : code, `The turn ended without an answer (${code}).`)\n }\n\n function voiceStore(): HostedAgentStore {\n if (!config.store) throw new HostedAgentError('store_missing', 'voice needs a store for call tokens')\n return config.store\n }\n\n return {\n /**\n * Attach an Inkbox iMessage identity, connected to Hub under the\n * developer's account, as this agent's line: the owner and anyone who\n * texts it each get their own box and thread. Safe to repeat with the\n * same config. Remove any Hub event subscription on the connection first;\n * Hub refuses a line that another route would also answer.\n */\n async attachLine(connectionId: string): Promise<LineAttachment> {\n const line = await sandbox.lines.fromConnection({ connectionId, transport: 'imessage', clientReference: 'hosted-agent' })\n return sandbox.lines.attach({\n number: line.id,\n mode: 'shared',\n members: [{ address: config.owner, role: 'owner' }],\n unknownSenders: 'guest',\n roles: { owner: PERSON, guest: PERSON },\n respond: { kind: 'agent', backend },\n limits: { turnsPerMemberPerDay: config.freeTurnsPerDay ?? 20 },\n instance: { keyPrefix: PERSON_KEY_PREFIX, create },\n clientReference: 'hosted-agent',\n })\n },\n\n /**\n * ph0ny's call hook. `admit` returns a call token bound to the caller's\n * number from the carrier's signed call record, never from the model.\n */\n async voiceHook(request: Request): Promise<Response> {\n const secret = config.voiceSecret\n if (!secret || !await equal(request.headers.get('authorization'), `Bearer ${secret}`)) return json({ error: 'unauthorized' }, 401)\n const body = await request.json().catch(() => null) as { event?: string; phone?: string } | null\n if (body?.event !== 'admit') return json({ ok: true })\n const phone = body.phone?.trim() ?? ''\n if (!E164.test(phone)) return json({ admit: false, say: 'Sorry, I can only take calls from a visible number.' })\n // ph0ny accepts only an opaque token matching ^[A-Za-z0-9_-]{16,128}$,\n // so the caller's number stays here and the token is its random key.\n const callToken = randomToken()\n await voiceStore().put(`vcall:${callToken}`, JSON.stringify({ phone, exp: Date.now() + CALL_TTL_MS } satisfies Call),\n { expirationTtl: CALL_TTL_MS / 1000 })\n return json({ admit: true, callToken })\n },\n\n /**\n * ph0ny's `ask_workspace` webhook tool, configured with `forwardCallToken`.\n * A slow turn returns a ticket; the voice agent asks again with it.\n */\n async voiceAsk(request: Request): Promise<Response> {\n const secret = config.voiceSecret\n if (!secret || !await equal(request.headers.get('authorization'), `Bearer ${secret}`)) return json({ error: 'unauthorized' }, 401)\n const token = request.headers.get('x-voice-call-token') ?? ''\n const store = voiceStore()\n const admitted = /^[A-Za-z0-9_-]{16,128}$/.test(token) ? await store.get(`vcall:${token}`) : null\n if (!admitted) return json({ status: 'error', error: 'call_not_admitted' }, 403)\n const call = JSON.parse(admitted) as Call\n if (call.exp < Date.now()) return json({ status: 'error', error: 'call_expired' }, 403)\n const body = await request.json().catch(() => null) as { utterance?: string; ticket?: string } | null\n // A ticket carries its own turn id and question, so a retry after a\n // slow first step still settles exactly one turn.\n const [turnId, asked] = body?.ticket ? body.ticket.split('.') : [`v-${crypto.randomUUID()}`, b64(body?.utterance ?? '')]\n if (!turnId || !asked) return json({ status: 'error', error: 'missing_utterance' }, 400)\n try {\n if (!call.session) {\n call.session = await callSession(call.phone)\n await store.put(`vcall:${token}`, JSON.stringify(call), { expirationTtl: Math.max(60, Math.ceil((call.exp - Date.now()) / 1000)) })\n }\n // ph0ny drops the tool call at its own timeout, and resuming an idle\n // box can take longer than the budget. Answer 'pending' at the\n // budget regardless; every step is idempotent by turn id, so the\n // ticket call continues where this one stopped.\n const budgetMs = config.voiceBudgetMs ?? 8000\n const result = await Promise.race([\n answerCall(call, unb64(asked), turnId, Date.now() + budgetMs),\n sleep(budgetMs).then((): AskResult => ({ state: 'pending' })),\n ])\n if (result.state === 'pending') return json({ status: 'pending', ticket: `${turnId}.${asked}` })\n if (result.state === 'declined') return json({ status: 'complete', answer: result.reply ?? 'I cannot answer that right now.' })\n return json({ status: 'complete', answer: result.text })\n } catch (error) {\n console.error(`[hosted-agent] voice turn=${turnId} ${describe(error)}`)\n return json({ status: 'error', error: 'agent_unavailable' }, 502)\n }\n },\n }\n}\n\nexport type HostedAgent = ReturnType<typeof createHostedAgent>\n","import type { BackendConfig, CreateSessionOptions } from '@tangle-network/sandbox'\nimport type { ConversationTurnOptions, ConversationTurnResult, PromptInputPart } from '@tangle-network/sandbox/core'\n\n/**\n * The one turn engine for a hosted agent, whatever hosts it. This kit's\n * `createHostedAgent` runs on it with a key-value store, and Agent Builder\n * runs on it with its D1 enrollments, per-user keys, spaces and product\n * ingest. A host supplies those as ports; the engine owns the order of the\n * steps and what each outcome and failure means.\n *\n * One call is one non-blocking pass for one message:\n *\n * validate -> admit once -> the person's box -> authorize -> session -> drive -> classify\n *\n * Every step is idempotent by the turn id: the sandbox's admission receipt\n * settles a repeated drive on the same turn, so a host may repeat a pass for\n * the same message any number of times and the agent runs it once.\n */\n\nexport type TurnRefusal = 'not_enrolled' | 'execution_not_ready' | 'refused' | 'unavailable'\nexport type TurnReply = { ok: true; text: string } | { ok: false; reason: TurnRefusal; detail?: string }\nexport type TurnStep = 'ensure' | 'drive' | 'turn'\n/**\n * Where an unfinished turn stands. `code` is the same on every repeat of the\n * same cause, so a host can tell a stuck turn from a slow one. A transient\n * cause (a lost response, a timeout, a busy platform, a box still starting)\n * is expected to clear by itself; any other repeats until it is fixed.\n */\nexport interface PendingDetail { step: TurnStep; code: string; transient: boolean }\n/** `cause` is what a port or the Sandbox threw, when a throw left the turn pending. */\nexport type TurnOutcome = TurnReply | { ok: 'pending'; detail: PendingDetail; cause?: unknown }\n\nexport const MAX_MESSAGE_CHARS = 8000\n/** Identical non-transient failures in a row after which a host fails a turn. */\nexport const MAX_REPEATED_FAILURES = 3\n/** Refusal detail when the host's `authorize` check fails. */\nexport const AUTHORITY_ENDED = 'authority_ended'\n\n/** The part of a Sandbox box one turn uses. `SandboxInstance` satisfies it. */\nexport interface TurnBox {\n readonly id: string\n driveConversationTurn(message: string | PromptInputPart[], options: ConversationTurnOptions): Promise<ConversationTurnResult>\n session(id: string): { status(): Promise<unknown> }\n createSession(options: CreateSessionOptions): Promise<unknown>\n}\n\n/** A port throws this to report a turn that is not ready yet, such as a box still starting. */\nexport class TurnPending extends Error {\n constructor(readonly code: string, readonly transient: boolean) {\n super(code)\n this.name = 'TurnPending'\n }\n}\n\n/** A thrown value as a stable code plus a secret-free message for a log line. */\nexport interface TurnFailure { name: string; code: string; status?: number; reason?: string; message: string }\n\n/** What a pass reports while it runs. Every hook is optional; none changes the outcome. */\nexport interface TurnObserver {\n /** Time one step of this pass took, whether it returned or threw. */\n span?(step: 'ensure' | 'session' | 'drive', ms: number): void\n /** The session the turn runs in, as the runtime reports it, or the backend it was created with. */\n session?(info: { harness?: string; model?: string; created: boolean }): void\n /**\n * Read a session this pass created once more, so `session` reports the\n * harness and model the runtime chose for it. It costs one request, so a\n * host asks for it only when it shows them (an owner's debug line, say).\n */\n readCreatedSession?: boolean\n /** Each drive result and the time it arrived. */\n drive?(result: ConversationTurnResult, at: number): void\n /** A failure that left the turn pending or refused. */\n failure?(step: TurnStep, failure: TurnFailure, boxId?: string): void | Promise<void>\n}\n\n/** Storage and policy a host supplies for one message. */\nexport interface HostedTurnPorts<Box extends TurnBox = TurnBox> {\n /**\n * Admit this turn once, before any compute. Return null to run it, or the\n * refusal. A turn admitted by an earlier pass must return null again, so a\n * retry never consumes allowance twice.\n */\n admit(): Promise<Extract<TurnReply, { ok: false }> | null>\n /** The person's running box: created, resumed or replaced as needed. Throw to report why not. */\n box(): Promise<Box>\n /** The host's own authority (a phone link, say), read after the box is ready and right before the drive. */\n authorize?(): Promise<boolean>\n /** The session this turn runs in. A session binds its backend when created, so a new persona needs a new id. */\n sessionId: string\n /** The backend a missing session is created with. Without it the runtime answers as its generic assistant. */\n backend(): Promise<BackendConfig>\n /** The message as the agent receives it: text, or text and images. */\n prompt(box: Box): Promise<string | PromptInputPart[]>\n /** Runs once the turn has a non-empty answer, before the pass returns it. */\n answered?(answer: string, box: Box): Promise<void>\n /** Map a thrown value to an outcome. Undefined leaves it to the engine's own classification. */\n classify?(error: unknown, step: 'ensure' | 'drive'): TurnOutcome | undefined\n observe?: TurnObserver\n}\n\nexport interface HostedTurnOptions {\n /** The run's wall-clock cap; the Sandbox cancels a turn that exceeds it. */\n wallCapMs: number\n /** How long one drive request may wait on a running turn. Default 8 s. */\n timeoutMs?: number\n /** False only reads a turn admitted earlier; it never admits or starts one. */\n allowDispatch?: boolean\n /** Keep driving a running turn until this time, instead of returning pending after one drive. */\n until?: number\n /** Wait between drives while `until` allows. Default 2 s. */\n pollMs?: number\n now?: () => number\n sleep?: (ms: number) => Promise<void>\n}\n\n/** Harness and model as the runtime reports them; the model it selected after normalization wins. */\nfunction sessionInfo(session: unknown): { harness?: string; model?: string } {\n const s = (typeof session === 'object' && session !== null ? session : {}) as\n { backend?: unknown; model?: unknown; effectiveBackend?: { model?: unknown } | null }\n const effective = s.effectiveBackend?.model\n const model = typeof effective === 'string' && effective ? effective : typeof s.model === 'string' && s.model ? s.model : undefined\n return { ...(typeof s.backend === 'string' && s.backend ? { harness: s.backend } : {}), ...(model ? { model } : {}) }\n}\n\nconst refusal = (reason: TurnRefusal, detail?: string): Extract<TurnReply, { ok: false }> => ({ ok: false, reason, ...(detail ? { detail } : {}) })\nconst RUNNING = new Set(['running', 'awaiting_input', 'not_admitted'])\nconst TRANSIENT_ERRORS = new Set(['NetworkError', 'TimeoutError', 'QuotaError', 'AbortError', 'EdgeNotReadyError'])\n/**\n * The 503 codes the Sandbox SDK itself classifies as refused before dispatch\n * and safe to retry. A 503 with any other code, such as\n * HUB_CREDENTIAL_REFRESH_FAILED, has not shown it will clear by itself.\n */\nconst TRANSIENT_503 = new Set(['MODEL_CREDENTIAL_SUPERSEDED', 'HUB_CREDENTIAL_REFRESH_UNAVAILABLE'])\n\n/** The code omits the message, which can carry ids or times that differ on every repeat of one cause. */\nexport function describeFailure(error: unknown): TurnFailure {\n const e = (typeof error === 'object' && error !== null ? error : {}) as Record<string, unknown>\n const name = typeof e.name === 'string' ? e.name : typeof error\n const status = typeof e.status === 'number' ? e.status : undefined\n const own = typeof e.code === 'string' ? e.code : typeof e.reason === 'string' ? e.reason : undefined\n const code = [name, own, status].filter(part => part !== undefined).join(':').replace(/[^\\w.:-]/g, '_').slice(0, 100)\n const message = String(typeof e.message === 'string' ? e.message : error)\n .replace(/sk-tan-[\\w-]+|eyJ[\\w-]+\\.[\\w-]+\\.[\\w-]*|Bearer\\s+\\S+/g, '<redacted>').slice(0, 400)\n return { name, code, ...(status !== undefined ? { status } : {}), ...(own !== undefined ? { reason: own } : {}), message }\n}\n\nexport function isTransientFailure(failure: Pick<TurnFailure, 'name' | 'status' | 'reason'>): boolean {\n return TRANSIENT_ERRORS.has(failure.name) || [408, 429, 502, 504].includes(failure.status ?? 0) ||\n (failure.status === 503 && (failure.reason === undefined || TRANSIENT_503.has(failure.reason)))\n}\n\n/**\n * The platform refuses a funded request once the paying key's budget is\n * spent. That refusal is decided, so retrying it would loop forever.\n */\nconst budgetExhausted = (error: unknown) => error instanceof Error && error.name === 'KeyBudgetExhaustedError'\n\n/** One non-blocking pass for one message. Safe to repeat with the same turn id. */\nexport async function runHostedTurn<Box extends TurnBox>(turn: { turnId: string; text: string }, ports: HostedTurnPorts<Box>,\n options: HostedTurnOptions): Promise<TurnOutcome> {\n const now = options.now ?? Date.now\n const sleep = options.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const observe = ports.observe ?? {}\n const allowDispatch = options.allowDispatch !== false\n const text = turn.text.trim()\n if (!text || text.length > MAX_MESSAGE_CHARS || !turn.turnId || turn.turnId.length > 200) return refusal('refused', 'invalid_message')\n const refused = await ports.admit()\n if (refused) return refused\n\n const timed = async <T>(step: 'ensure' | 'session' | 'drive', fn: () => Promise<T>): Promise<T> => {\n const start = now()\n try { return await fn() } finally { observe.span?.(step, now() - start) }\n }\n const failed = async (step: 'ensure' | 'drive', error: unknown, boxId?: string): Promise<TurnOutcome> => {\n const failure = describeFailure(error)\n await observe.failure?.(step, failure, boxId)\n const classified = ports.classify?.(error, step)\n if (classified) return classified\n if (error instanceof TurnPending) return { ok: 'pending', detail: { step, code: error.code, transient: error.transient } }\n if (budgetExhausted(error)) return refusal('refused', 'budget_exhausted')\n // An admission whose response was lost is retried with the same turn id;\n // the runtime's admission receipt prevents a second execution.\n return { ok: 'pending', detail: { step, code: failure.code, transient: isTransientFailure(failure) }, cause: error }\n }\n\n let box: Box\n try {\n box = await timed('ensure', () => ports.box())\n } catch (error) {\n return failed('ensure', error)\n }\n if (ports.authorize && !await ports.authorize()) return refusal('refused', AUTHORITY_ENDED)\n\n let result: ConversationTurnResult\n try {\n const prompt = await ports.prompt(box)\n const sessionId = ports.sessionId\n // The SDK would create a missing session with no profile, and the runtime\n // would then answer as its generic default assistant.\n if (allowDispatch) await timed('session', async () => {\n const session = await box.session(sessionId).status()\n if (session) {\n observe.session?.({ created: false, ...sessionInfo(session) })\n return\n }\n const backend = await ports.backend()\n await box.createSession({ sessionId, retention: 'workspace', backend })\n // A new session names its harness and model only once the runtime has created it.\n const created = observe.session && observe.readCreatedSession ? sessionInfo(await box.session(sessionId).status()) : {}\n observe.session?.({ created: true, ...(backend.type ? { harness: backend.type } : {}), ...created })\n })\n for (;;) {\n result = await timed('drive', () => box.driveConversationTurn(prompt, { sessionId, turnId: turn.turnId,\n wallCapMs: options.wallCapMs, timeoutMs: options.timeoutMs ?? 8000, allowDispatch }))\n observe.drive?.(result, now())\n const pollMs = options.pollMs ?? 2000\n if (!RUNNING.has(result.state) || options.until === undefined || now() + pollMs > options.until) break\n await sleep(pollMs)\n }\n } catch (error) {\n return failed('drive', error, box.id)\n }\n switch (result.state) {\n case 'completed': {\n const answer = result.text.trim()\n if (!answer) {\n await observe.failure?.('turn', describeFailure(new Error('The turn completed with no reply text')), box.id)\n return refusal('unavailable', 'empty_reply')\n }\n await ports.answered?.(answer, box)\n return { ok: true, text: answer }\n }\n case 'failed':\n await observe.failure?.('turn', describeFailure(Object.assign(new Error(result.error), { name: 'TurnFailed' })), box.id)\n return refusal('unavailable', 'agent_failed')\n // Interaction and plan approval are not exposed over messaging. The wall\n // cap cancels a turn that waits on one, which frees the session.\n // Sandbox 0.45 reported approval and question outcomes as `failed`.\n case 'awaiting_plan_decision': case 'blocked_on_approval': case 'awaiting_question': case 'awaiting_interaction':\n return refusal('refused', 'needs_decision')\n case 'running': case 'awaiting_input': case 'not_admitted':\n return { ok: 'pending', detail: { step: 'turn', code: result.state, transient: true } }\n }\n}\n\n/**\n * Repeat passes until the turn settles, for a caller that holds a connection\n * open (a voice question, say). A turn the runtime never finishes must not\n * hold the caller forever, so `deadline` bounds the wait; `signal` stops the\n * wait only, and the admitted turn keeps running.\n */\nexport async function settleHostedTurn(pass: () => Promise<TurnOutcome>, options: {\n deadline: number\n pollMs?: number\n signal?: AbortSignal\n now?: () => number\n sleep?: (ms: number, signal?: AbortSignal) => Promise<void>\n}): Promise<TurnReply> {\n const now = options.now ?? Date.now\n const sleep = options.sleep ?? ((ms: number, signal?: AbortSignal) => new Promise<void>((resolve, reject) => {\n const timer = setTimeout(resolve, ms)\n signal?.addEventListener('abort', () => { clearTimeout(timer); reject(signal.reason) }, { once: true })\n }))\n let last = '', repeats = 0\n for (;;) {\n if (options.signal?.aborted || now() > options.deadline) return refusal('unavailable', 'pending')\n const outcome = await pass()\n if (outcome.ok !== 'pending') return outcome\n repeats = outcome.detail.code === last ? repeats + 1 : 1\n last = outcome.detail.code\n if (!outcome.detail.transient && repeats >= MAX_REPEATED_FAILURES) return refusal('unavailable', `${outcome.detail.step}:${last}`)\n try { await sleep(options.pollMs ?? 1500, options.signal) } catch { return refusal('unavailable', 'pending') }\n }\n}\n\n/**\n * A space member who may chat only: no shell and no file writes in the shared\n * box. The profile's other settings are kept.\n */\nexport function chatOnlyBackend(backend: BackendConfig): BackendConfig {\n const profile = backend.profile ?? {}\n return { ...backend, profile: { ...profile,\n tools: { ...profile.tools, bash: false, edit: false, write: false },\n permissions: { ...profile.permissions, bash: 'deny', edit: 'deny' } } }\n}\n"],"mappings":"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","names":["sleep"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.49.
|
|
3
|
+
"version": "0.49.18",
|
|
4
4
|
"packageManager": "pnpm@11.24.0",
|
|
5
5
|
"description": "Build agent applications with typed chat, tools, sandboxes, integrations, billing, and evaluation.",
|
|
6
6
|
"keywords": [
|
|
@@ -546,10 +546,9 @@
|
|
|
546
546
|
"@tangle-network/agent-interface": "2.12.0",
|
|
547
547
|
"@tangle-network/agent-knowledge": "17.1.2",
|
|
548
548
|
"@tangle-network/agent-profile-materialize": "0.20.1",
|
|
549
|
-
"@tangle-network/agent-runtime": "0.
|
|
549
|
+
"@tangle-network/agent-runtime": "0.266.0",
|
|
550
550
|
"@tangle-network/brand": "1.5.0",
|
|
551
|
-
"@tangle-network/
|
|
552
|
-
"@tangle-network/sandbox": "0.50.0",
|
|
551
|
+
"@tangle-network/sandbox": "0.52.1",
|
|
553
552
|
"@tangle-network/sandbox-ui": "0.113.3",
|
|
554
553
|
"@tangle-network/ui": "11.9.0",
|
|
555
554
|
"@testing-library/dom": "^10.4.1",
|
|
@@ -607,10 +606,9 @@
|
|
|
607
606
|
"@tangle-network/agent-interface": "^2.11.0",
|
|
608
607
|
"@tangle-network/agent-knowledge": "^17.1.2",
|
|
609
608
|
"@tangle-network/agent-profile-materialize": ">=0.20.1 <0.21.0",
|
|
610
|
-
"@tangle-network/agent-runtime": ">=0.262.0 <0.
|
|
609
|
+
"@tangle-network/agent-runtime": ">=0.262.0 <0.267.0",
|
|
611
610
|
"@tangle-network/brand": ">=1.5.0",
|
|
612
|
-
"@tangle-network/
|
|
613
|
-
"@tangle-network/sandbox": ">=0.45.0 <0.48.0 || ^0.49.0 || ^0.50.0",
|
|
611
|
+
"@tangle-network/sandbox": ">=0.45.0 <0.48.0 || ^0.49.0 || ^0.50.0 || ^0.51.0 || ^0.52.0",
|
|
614
612
|
"@tangle-network/sandbox-ui": ">=0.113.3 <0.114.0",
|
|
615
613
|
"@tangle-network/ui": ">=11.6.0 <12.0.0",
|
|
616
614
|
"@tiptap/core": ">=3.28.0 <4.0.0",
|
|
@@ -709,9 +707,6 @@
|
|
|
709
707
|
},
|
|
710
708
|
"react-dom": {
|
|
711
709
|
"optional": true
|
|
712
|
-
},
|
|
713
|
-
"@tangle-network/hub-sdk": {
|
|
714
|
-
"optional": true
|
|
715
710
|
}
|
|
716
711
|
},
|
|
717
712
|
"dependencies": {
|