@tangle-network/agent-app 0.49.4 → 0.49.5
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.
|
@@ -43,6 +43,10 @@ import {
|
|
|
43
43
|
selectTickStep
|
|
44
44
|
} from "../chunk-KWN5PGXM.js";
|
|
45
45
|
import "../chunk-Q2QKEV6J.js";
|
|
46
|
+
import {
|
|
47
|
+
DesignCanvasChromeLazy,
|
|
48
|
+
DesignCanvasLazy
|
|
49
|
+
} from "../chunk-WAMJSZYB.js";
|
|
46
50
|
import "../chunk-ZN5J47UX.js";
|
|
47
51
|
import {
|
|
48
52
|
DEFAULT_INSERT_TEMPLATES,
|
|
@@ -90,10 +94,6 @@ import {
|
|
|
90
94
|
bleedAwareExportBounds,
|
|
91
95
|
scaleForPreset
|
|
92
96
|
} from "../chunk-BWQPVS7D.js";
|
|
93
|
-
import {
|
|
94
|
-
DesignCanvasChromeLazy,
|
|
95
|
-
DesignCanvasLazy
|
|
96
|
-
} from "../chunk-WAMJSZYB.js";
|
|
97
97
|
import "../chunk-EA4UVS4T.js";
|
|
98
98
|
import "../chunk-TXD5HXLE.js";
|
|
99
99
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { type HubProviderEvent } from '@tangle-network/hub-sdk';
|
|
2
|
+
import type { AgentProfile } from '@tangle-network/sandbox';
|
|
3
|
+
/**
|
|
4
|
+
* A hosted agent: a person texts or calls a line, and the agent answers from
|
|
5
|
+
* that person's own isolated sandbox. The developer's Tangle API key pays for
|
|
6
|
+
* every box, model turn and reply.
|
|
7
|
+
*
|
|
8
|
+
* Each person gets one fresh isolated box built from the published profile,
|
|
9
|
+
* never a copy of the developer's box. Later messages resume the same box and
|
|
10
|
+
* the same conversation, so text and voice share one memory. A box the
|
|
11
|
+
* platform deleted is replaced by a fresh one instead of blocking the person.
|
|
12
|
+
*
|
|
13
|
+
* Every step is idempotent by the message's turn id: a retried delivery
|
|
14
|
+
* settles the same sandbox turn and replays the same Hub send. So the caller
|
|
15
|
+
* may retry any failure, and needs no dedupe table.
|
|
16
|
+
*/
|
|
17
|
+
/** Durable key-value storage. A Cloudflare KV namespace satisfies it. */
|
|
18
|
+
export interface HostedAgentStore {
|
|
19
|
+
get(key: string): Promise<string | null>;
|
|
20
|
+
put(key: string, value: string, options?: {
|
|
21
|
+
expirationTtl?: number;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export type HostedChannel = 'imessage' | 'voice';
|
|
25
|
+
export interface HostedMessage {
|
|
26
|
+
/** The sender's address as the channel delivered it: E.164 for a phone. */
|
|
27
|
+
userId: string;
|
|
28
|
+
channel: HostedChannel;
|
|
29
|
+
text: string;
|
|
30
|
+
/** Stable per logical message. Reusing it settles the same turn. */
|
|
31
|
+
turnId: string;
|
|
32
|
+
}
|
|
33
|
+
/** What the allowance hook decides for one new message. */
|
|
34
|
+
export type Allowance = 'answer' | 'ignore' | {
|
|
35
|
+
reply: string;
|
|
36
|
+
};
|
|
37
|
+
export interface HostedAgentConfig {
|
|
38
|
+
/** The developer's Tangle API key (Sandbox and Hub). */
|
|
39
|
+
apiKey: string;
|
|
40
|
+
/** The persona, model and tools every person's box runs. */
|
|
41
|
+
profile: AgentProfile;
|
|
42
|
+
/** Backend harness type, such as `opencode`; the runtime default when omitted. */
|
|
43
|
+
harness?: string;
|
|
44
|
+
store: HostedAgentStore;
|
|
45
|
+
/** Answers per person per UTC day before `allow` must decide. Default 20. */
|
|
46
|
+
freeTurnsPerDay?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Decide a new message after the free allowance is used, for example reply
|
|
49
|
+
* with a checkout link. Without it, a person past the allowance is told to
|
|
50
|
+
* come back tomorrow. Retries of an admitted message never reach this hook.
|
|
51
|
+
*/
|
|
52
|
+
allow?: (message: HostedMessage, usedToday: number) => Allowance | Promise<Allowance>;
|
|
53
|
+
/** Shared secret for the ph0ny call hook and `ask_workspace` webhook tool. */
|
|
54
|
+
voiceSecret?: string;
|
|
55
|
+
/** How long one `ask_workspace` call may wait before it returns a ticket. Default 8 s. */
|
|
56
|
+
voiceBudgetMs?: number;
|
|
57
|
+
box?: Partial<BoxPolicy>;
|
|
58
|
+
turnWallCapMs?: number;
|
|
59
|
+
sandboxUrl?: string;
|
|
60
|
+
hubUrl?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface BoxPolicy {
|
|
63
|
+
cpuCores: number;
|
|
64
|
+
memoryMB: number;
|
|
65
|
+
diskGB: number;
|
|
66
|
+
idleTimeoutSeconds: number;
|
|
67
|
+
maxLifetimeSeconds: number;
|
|
68
|
+
deleteAfterStoppedSeconds: number;
|
|
69
|
+
/** Egress allow-list. The default reaches the model router only. */
|
|
70
|
+
allowDomains: string[];
|
|
71
|
+
}
|
|
72
|
+
export declare const DEFAULT_BOX_POLICY: BoxPolicy;
|
|
73
|
+
export type AskResult = {
|
|
74
|
+
state: 'answered';
|
|
75
|
+
text: string;
|
|
76
|
+
} | {
|
|
77
|
+
state: 'declined';
|
|
78
|
+
reply?: string;
|
|
79
|
+
} | {
|
|
80
|
+
state: 'pending';
|
|
81
|
+
};
|
|
82
|
+
/** A Hub delivery that passed authentication, in a form a queue can carry. */
|
|
83
|
+
export interface HostedInbound {
|
|
84
|
+
runId: string;
|
|
85
|
+
connectionId: string;
|
|
86
|
+
event: HubProviderEvent;
|
|
87
|
+
}
|
|
88
|
+
export declare const NOTICE: {
|
|
89
|
+
readonly stopped: 'You are unsubscribed. Text START to talk again.';
|
|
90
|
+
readonly started: 'Welcome back. Text me anytime. Reply STOP to stop.';
|
|
91
|
+
readonly limit: 'You have used today\'s free messages. Talk tomorrow!';
|
|
92
|
+
readonly unavailable: 'Sorry, I could not answer that just now. Please try again in a minute.';
|
|
93
|
+
};
|
|
94
|
+
/** One address per person: E.164 for anything phone-shaped, else lowercase. */
|
|
95
|
+
export declare function normalizeAddress(value: string): string | null;
|
|
96
|
+
export declare class HostedAgentError extends Error {
|
|
97
|
+
readonly code: string;
|
|
98
|
+
constructor(code: string, message: string);
|
|
99
|
+
}
|
|
100
|
+
export declare function createHostedAgent(config: HostedAgentConfig): {
|
|
101
|
+
ask: (message: HostedMessage, options: {
|
|
102
|
+
deadline: number;
|
|
103
|
+
}) => Promise<AskResult>;
|
|
104
|
+
/**
|
|
105
|
+
* One-time setup: route an Inkbox iMessage identity's messages, connected
|
|
106
|
+
* to Hub under the developer's account, to `callbackUrl`, and allow the
|
|
107
|
+
* one reply action on that connection. Safe to repeat.
|
|
108
|
+
*/
|
|
109
|
+
connect(input: {
|
|
110
|
+
connectionId: string;
|
|
111
|
+
identityId: string;
|
|
112
|
+
callbackUrl: string;
|
|
113
|
+
secret: string;
|
|
114
|
+
}): Promise<import("@tangle-network/hub-sdk").HubEventSubscription>;
|
|
115
|
+
/** The Hub callback. Authenticates within Hub's 10 s budget and runs nothing. */
|
|
116
|
+
receive(request: Request, secret: string): Promise<{
|
|
117
|
+
response: Response;
|
|
118
|
+
inbound?: HostedInbound;
|
|
119
|
+
}>;
|
|
120
|
+
/**
|
|
121
|
+
* Answer one inbound message and send the reply through Hub. Returns
|
|
122
|
+
* 'pending' while the turn still runs; call again with the same inbound.
|
|
123
|
+
* On `lastAttempt`, a failure is answered with an apology instead of thrown.
|
|
124
|
+
*/
|
|
125
|
+
respond(inbound: HostedInbound, options?: {
|
|
126
|
+
lastAttempt?: boolean;
|
|
127
|
+
}): Promise<'replied' | 'ignored' | 'pending'>;
|
|
128
|
+
/**
|
|
129
|
+
* ph0ny's call hook. `admit` returns a call token bound to the caller's
|
|
130
|
+
* number from the carrier's signed call record, never from the model.
|
|
131
|
+
*/
|
|
132
|
+
voiceHook(request: Request): Promise<Response>;
|
|
133
|
+
/**
|
|
134
|
+
* ph0ny's `ask_workspace` webhook tool, configured with `forwardCallToken`.
|
|
135
|
+
* A slow turn returns a ticket; the voice agent asks again with it.
|
|
136
|
+
*/
|
|
137
|
+
voiceAsk(request: Request): Promise<Response>;
|
|
138
|
+
};
|
|
139
|
+
export type HostedAgent = ReturnType<typeof createHostedAgent>;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// src/hosted-agent/index.ts
|
|
2
|
+
import { authenticateHubEventRequest, HubClient } from "@tangle-network/hub-sdk";
|
|
3
|
+
import { buildMessagingReply, normalizeConversationEvent } from "@tangle-network/agent-integrations/conversation-events";
|
|
4
|
+
import { Sandbox } from "@tangle-network/sandbox/core";
|
|
5
|
+
var DEFAULT_BOX_POLICY = {
|
|
6
|
+
cpuCores: 1,
|
|
7
|
+
memoryMB: 2048,
|
|
8
|
+
diskGB: 10,
|
|
9
|
+
idleTimeoutSeconds: 600,
|
|
10
|
+
maxLifetimeSeconds: 86400,
|
|
11
|
+
deleteAfterStoppedSeconds: 7 * 86400,
|
|
12
|
+
allowDomains: ["router.tangle.tools"]
|
|
13
|
+
};
|
|
14
|
+
var NOTICE = {
|
|
15
|
+
stopped: "You are unsubscribed. Text START to talk again.",
|
|
16
|
+
started: "Welcome back. Text me anytime. Reply STOP to stop.",
|
|
17
|
+
limit: "You have used today's free messages. Talk tomorrow!",
|
|
18
|
+
unavailable: "Sorry, I could not answer that just now. Please try again in a minute."
|
|
19
|
+
};
|
|
20
|
+
var IMESSAGE_EVENT = "inkbox.imessage.received";
|
|
21
|
+
var IMESSAGE_REPLY = "inkbox.imessage.reply";
|
|
22
|
+
var STOP = /^(stop|stopall|unsubscribe|cancel|end|quit)$/i;
|
|
23
|
+
var START = /^(start|unstop|resume)$/i;
|
|
24
|
+
var TWO_DAYS = 2 * 86400;
|
|
25
|
+
var RESUMABLE = /* @__PURE__ */ new Set(["stopped", "expired"]);
|
|
26
|
+
var GONE = /* @__PURE__ */ new Set(["failed", "deleted"]);
|
|
27
|
+
var encode = (text) => new TextEncoder().encode(text);
|
|
28
|
+
var hex = (bytes) => Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
var sha256 = async (value) => hex(await crypto.subtle.digest("SHA-256", encode(value)));
|
|
30
|
+
async function hmac(secret, value) {
|
|
31
|
+
const key = await crypto.subtle.importKey("raw", encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
32
|
+
return hex(await crypto.subtle.sign("HMAC", key, encode(value)));
|
|
33
|
+
}
|
|
34
|
+
async function equal(given, expected) {
|
|
35
|
+
if (!given) return false;
|
|
36
|
+
const [a, b] = await Promise.all([sha256(given), sha256(expected)]);
|
|
37
|
+
let diff = 0;
|
|
38
|
+
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
39
|
+
return diff === 0;
|
|
40
|
+
}
|
|
41
|
+
var b64 = (text) => btoa(String.fromCharCode(...encode(text))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
42
|
+
var unb64 = (text) => new TextDecoder().decode(Uint8Array.from(atob(text.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0)));
|
|
43
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
var json = (body, status = 200) => Response.json(body, { status });
|
|
45
|
+
var describe = (error) => (error instanceof Error ? `${error.name}: ${error.message}` : String(error)).replace(/sk-tan-[\w-]+|Bearer\s+\S+/g, "<redacted>").slice(0, 400);
|
|
46
|
+
function normalizeAddress(value) {
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (trimmed.includes("@")) return trimmed.toLowerCase();
|
|
49
|
+
const digits = trimmed.replace(/[^\d]/g, "");
|
|
50
|
+
if (digits.length < 8 || digits.length > 15) return null;
|
|
51
|
+
return `+${digits.length === 10 && !trimmed.startsWith("+") ? `1${digits}` : digits}`;
|
|
52
|
+
}
|
|
53
|
+
var HostedAgentError = class extends Error {
|
|
54
|
+
constructor(code, message) {
|
|
55
|
+
super(message);
|
|
56
|
+
this.code = code;
|
|
57
|
+
this.name = "HostedAgentError";
|
|
58
|
+
}
|
|
59
|
+
code;
|
|
60
|
+
};
|
|
61
|
+
function createHostedAgent(config) {
|
|
62
|
+
const store = config.store;
|
|
63
|
+
const policy = { ...DEFAULT_BOX_POLICY, ...config.box };
|
|
64
|
+
const wallCapMs = config.turnWallCapMs ?? 12e4;
|
|
65
|
+
const sandbox = new Sandbox({ apiKey: config.apiKey, baseUrl: config.sandboxUrl ?? "https://sandbox.tangle.tools", timeoutMs: 2e4 });
|
|
66
|
+
const hub = new HubClient({ baseUrl: config.hubUrl ?? "https://id.tangle.tools", apiKey: config.apiKey });
|
|
67
|
+
const backend = { ...config.harness ? { type: config.harness } : {}, profile: config.profile };
|
|
68
|
+
const profileTag = sha256(JSON.stringify(config.profile)).then((hash) => hash.slice(0, 8));
|
|
69
|
+
async function ensureBox(user, deadline) {
|
|
70
|
+
const key = `box:${user}`;
|
|
71
|
+
const known = await store.get(key);
|
|
72
|
+
let box = known ? await sandbox.get(known) : null;
|
|
73
|
+
if (!box || GONE.has(box.status)) {
|
|
74
|
+
box = await sandbox.createIsolated({
|
|
75
|
+
name: `hosted-${user.slice(0, 24)}`,
|
|
76
|
+
idempotencyKey: known ? `hosted-${user}-after-${known}` : `hosted-${user}`,
|
|
77
|
+
resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },
|
|
78
|
+
backend,
|
|
79
|
+
secrets: [],
|
|
80
|
+
sshEnabled: false,
|
|
81
|
+
egressPolicy: { mode: "strict", allowDomains: policy.allowDomains, includeImplicitDomains: false },
|
|
82
|
+
idleTimeoutSeconds: policy.idleTimeoutSeconds,
|
|
83
|
+
maxLifetimeSeconds: policy.maxLifetimeSeconds,
|
|
84
|
+
deleteAfterStoppedSeconds: policy.deleteAfterStoppedSeconds,
|
|
85
|
+
metadata: { hostedUser: user }
|
|
86
|
+
});
|
|
87
|
+
await store.put(key, box.id);
|
|
88
|
+
}
|
|
89
|
+
if (RESUMABLE.has(box.status)) await box.resume();
|
|
90
|
+
if (box.status === "running") return box;
|
|
91
|
+
const timeoutMs = deadline - Date.now();
|
|
92
|
+
if (timeoutMs < 1e3) return null;
|
|
93
|
+
try {
|
|
94
|
+
return await sandbox.waitForRunning(box.id, { timeoutMs });
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof Error && error.name === "TimeoutError") return null;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function admit(message, user) {
|
|
101
|
+
const stopKey = `stop:${user}`;
|
|
102
|
+
if (message.channel !== "voice" && STOP.test(message.text.trim())) {
|
|
103
|
+
await store.put(stopKey, "1");
|
|
104
|
+
return { reply: NOTICE.stopped };
|
|
105
|
+
}
|
|
106
|
+
if (message.channel !== "voice" && START.test(message.text.trim())) {
|
|
107
|
+
await store.put(stopKey, "");
|
|
108
|
+
return { reply: NOTICE.started };
|
|
109
|
+
}
|
|
110
|
+
if (await store.get(stopKey)) return "ignore";
|
|
111
|
+
const dayKey = `turns:${user}:${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
112
|
+
const used = Number(await store.get(dayKey) ?? 0);
|
|
113
|
+
const decision = used < (config.freeTurnsPerDay ?? 20) ? "answer" : config.allow ? await config.allow(message, used) : { reply: NOTICE.limit };
|
|
114
|
+
if (decision === "answer") await store.put(dayKey, String(used + 1), { expirationTtl: TWO_DAYS });
|
|
115
|
+
return decision;
|
|
116
|
+
}
|
|
117
|
+
async function ask(message, options) {
|
|
118
|
+
const text = message.text.trim();
|
|
119
|
+
if (!text || text.length > 8e3 || !/^[\w-]{1,120}$/.test(message.turnId)) return { state: "declined" };
|
|
120
|
+
const user = (await sha256(message.userId)).slice(0, 32);
|
|
121
|
+
const admittedKey = `turn:${user}:${message.turnId}`;
|
|
122
|
+
if (!await store.get(admittedKey)) {
|
|
123
|
+
const decision = await admit(message, user);
|
|
124
|
+
if (decision === "ignore") return { state: "declined" };
|
|
125
|
+
if (decision !== "answer") return { state: "declined", reply: decision.reply };
|
|
126
|
+
await store.put(admittedKey, "1", { expirationTtl: TWO_DAYS });
|
|
127
|
+
}
|
|
128
|
+
const box = await ensureBox(user, options.deadline);
|
|
129
|
+
if (!box) return { state: "pending" };
|
|
130
|
+
const sessionId = `hosted-${user}-${await profileTag}`;
|
|
131
|
+
if (!await box.session(sessionId).status()) await box.createSession({ sessionId, retention: "workspace", backend });
|
|
132
|
+
const prompt = message.channel === "voice" ? `[Phone call. Answer in one to three short spoken sentences.]
|
|
133
|
+
${text}` : text;
|
|
134
|
+
for (; ; ) {
|
|
135
|
+
const result = await box.driveConversationTurn(prompt, { sessionId, turnId: message.turnId, wallCapMs, timeoutMs: 8e3 });
|
|
136
|
+
if (result.state === "completed") {
|
|
137
|
+
const answer = result.text.trim();
|
|
138
|
+
if (!answer) throw new HostedAgentError("empty_reply", "The turn completed with no reply text.");
|
|
139
|
+
return { state: "answered", text: answer };
|
|
140
|
+
}
|
|
141
|
+
if (result.state === "failed") throw new HostedAgentError("turn_failed", result.error);
|
|
142
|
+
if (Date.now() + 2e3 > options.deadline) return { state: "pending" };
|
|
143
|
+
await sleep(2e3);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
ask,
|
|
148
|
+
/**
|
|
149
|
+
* One-time setup: route an Inkbox iMessage identity's messages, connected
|
|
150
|
+
* to Hub under the developer's account, to `callbackUrl`, and allow the
|
|
151
|
+
* one reply action on that connection. Safe to repeat.
|
|
152
|
+
*/
|
|
153
|
+
async connect(input) {
|
|
154
|
+
const webhook = await hub.connections.inboundWebhook.get(input.connectionId);
|
|
155
|
+
if (!webhook.configured) await hub.connections.inboundWebhook.create(input.connectionId, { identityId: input.identityId, imessage: true });
|
|
156
|
+
await hub.permissions.set({ connectionId: input.connectionId, actionPath: IMESSAGE_REPLY, decision: "allow" });
|
|
157
|
+
const { subscription } = await hub.eventSubscriptions.create({
|
|
158
|
+
clientReference: `hosted-agent:${input.connectionId}`,
|
|
159
|
+
label: "Hosted agent messages",
|
|
160
|
+
source: { type: "connection", connectionId: input.connectionId },
|
|
161
|
+
event: IMESSAGE_EVENT,
|
|
162
|
+
callback: { url: input.callbackUrl, secret: input.secret }
|
|
163
|
+
});
|
|
164
|
+
return subscription;
|
|
165
|
+
},
|
|
166
|
+
/** The Hub callback. Authenticates within Hub's 10 s budget and runs nothing. */
|
|
167
|
+
async receive(request, secret) {
|
|
168
|
+
const authenticated = await authenticateHubEventRequest({ request, secret, maxBodyBytes: 64 * 1024 });
|
|
169
|
+
if (!authenticated.ok) return { response: authenticated.response };
|
|
170
|
+
const { runId, source, providerEvent } = authenticated.delivery;
|
|
171
|
+
const ack = new Response(null, { status: 204 });
|
|
172
|
+
if (source.kind !== "connection" || source.event !== IMESSAGE_EVENT || providerEvent.connectionId !== source.id) return { response: ack };
|
|
173
|
+
return { response: ack, inbound: { runId, connectionId: source.id, event: providerEvent } };
|
|
174
|
+
},
|
|
175
|
+
/**
|
|
176
|
+
* Answer one inbound message and send the reply through Hub. Returns
|
|
177
|
+
* 'pending' while the turn still runs; call again with the same inbound.
|
|
178
|
+
* On `lastAttempt`, a failure is answered with an apology instead of thrown.
|
|
179
|
+
*/
|
|
180
|
+
async respond(inbound, options = {}) {
|
|
181
|
+
const normalized = normalizeConversationEvent(inbound.event);
|
|
182
|
+
if (!normalized.ok) return "ignored";
|
|
183
|
+
const event = normalized.event;
|
|
184
|
+
const userId = normalizeAddress(event.sender.id ?? "");
|
|
185
|
+
if (!userId || !event.text?.trim() || event.isGroup || event.historyOnly) return "ignored";
|
|
186
|
+
const turnId = `t-${(await sha256(inbound.runId)).slice(0, 40)}`;
|
|
187
|
+
let text;
|
|
188
|
+
try {
|
|
189
|
+
const result = await ask(
|
|
190
|
+
{ userId, channel: "imessage", text: event.text, turnId },
|
|
191
|
+
{ deadline: Date.now() + wallCapMs + 6e4 }
|
|
192
|
+
);
|
|
193
|
+
if (result.state === "pending") return "pending";
|
|
194
|
+
if (result.state === "declined" && !result.reply) return "ignored";
|
|
195
|
+
text = result.state === "answered" ? result.text : result.reply;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (!(error instanceof HostedAgentError) && !options.lastAttempt) throw error;
|
|
198
|
+
console.error(`[hosted-agent] turn=${turnId} ${describe(error)}`);
|
|
199
|
+
text = NOTICE.unavailable;
|
|
200
|
+
}
|
|
201
|
+
const plan = buildMessagingReply(inbound.event, text.slice(0, 1500), `reply-${turnId}`);
|
|
202
|
+
if (!plan.ok) return "ignored";
|
|
203
|
+
await hub.tools.invoke(plan.reply.action, plan.reply.input, { connectionId: inbound.connectionId, idempotencyKey: plan.reply.idempotencyKey });
|
|
204
|
+
return "replied";
|
|
205
|
+
},
|
|
206
|
+
/**
|
|
207
|
+
* ph0ny's call hook. `admit` returns a call token bound to the caller's
|
|
208
|
+
* number from the carrier's signed call record, never from the model.
|
|
209
|
+
*/
|
|
210
|
+
async voiceHook(request) {
|
|
211
|
+
const secret = config.voiceSecret;
|
|
212
|
+
if (!secret || !await equal(request.headers.get("authorization"), `Bearer ${secret}`)) return json({ error: "unauthorized" }, 401);
|
|
213
|
+
const body = await request.json().catch(() => null);
|
|
214
|
+
if (body?.event !== "admit") return json({ ok: true });
|
|
215
|
+
const phone = normalizeAddress(body.phone ?? "");
|
|
216
|
+
if (!phone?.startsWith("+")) return json({ admit: false, say: "Sorry, I can only take calls from a visible number." });
|
|
217
|
+
if (await store.get(`stop:${(await sha256(phone)).slice(0, 32)}`)) return json({ admit: false, say: "You unsubscribed from this line." });
|
|
218
|
+
const claims = b64(JSON.stringify({ phone, exp: Date.now() + 2 * 36e5 }));
|
|
219
|
+
return json({ admit: true, callToken: `hvt.${claims}.${await hmac(secret, claims)}` });
|
|
220
|
+
},
|
|
221
|
+
/**
|
|
222
|
+
* ph0ny's `ask_workspace` webhook tool, configured with `forwardCallToken`.
|
|
223
|
+
* A slow turn returns a ticket; the voice agent asks again with it.
|
|
224
|
+
*/
|
|
225
|
+
async voiceAsk(request) {
|
|
226
|
+
const secret = config.voiceSecret;
|
|
227
|
+
if (!secret || !await equal(request.headers.get("authorization"), `Bearer ${secret}`)) return json({ error: "unauthorized" }, 401);
|
|
228
|
+
const [tag, claims, signature] = (request.headers.get("x-voice-call-token") ?? "").split(".");
|
|
229
|
+
if (tag !== "hvt" || !claims || !await equal(signature, await hmac(secret, claims))) return json({ status: "error", error: "call_not_admitted" }, 403);
|
|
230
|
+
const call = JSON.parse(unb64(claims));
|
|
231
|
+
if (call.exp < Date.now()) return json({ status: "error", error: "call_expired" }, 403);
|
|
232
|
+
const body = await request.json().catch(() => null);
|
|
233
|
+
const [turnId, asked] = body?.ticket ? body.ticket.split(".") : [`v-${crypto.randomUUID()}`, b64(body?.utterance ?? "")];
|
|
234
|
+
if (!turnId || !asked) return json({ status: "error", error: "missing_utterance" }, 400);
|
|
235
|
+
try {
|
|
236
|
+
const result = await ask({ userId: call.phone, channel: "voice", text: unb64(asked), turnId }, { deadline: Date.now() + (config.voiceBudgetMs ?? 8e3) });
|
|
237
|
+
if (result.state === "pending") return json({ status: "pending", ticket: `${turnId}.${asked}` });
|
|
238
|
+
if (result.state === "declined") return json({ status: "complete", answer: result.reply ?? "I cannot answer that right now." });
|
|
239
|
+
return json({ status: "complete", answer: result.text });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error(`[hosted-agent] voice turn=${turnId} ${describe(error)}`);
|
|
242
|
+
return json({ status: "error", error: "agent_unavailable" }, 502);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
DEFAULT_BOX_POLICY,
|
|
249
|
+
HostedAgentError,
|
|
250
|
+
NOTICE,
|
|
251
|
+
createHostedAgent,
|
|
252
|
+
normalizeAddress
|
|
253
|
+
};
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/hosted-agent/index.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 { Sandbox } from '@tangle-network/sandbox/core'\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 * Each person gets one fresh isolated box built from the published profile,\n * never a copy of the developer's box. Later messages resume the same box and\n * the same conversation, so text and voice share one memory. A box the\n * platform deleted is replaced by a fresh one instead of blocking the person.\n *\n * Every step is idempotent by the message's turn id: a retried delivery\n * settles the same sandbox turn and replays the same Hub send. So the caller\n * may retry any failure, and needs no dedupe table.\n */\n\n/** Durable key-value storage. 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 /** The persona, model and tools every person's box runs. */\n profile: AgentProfile\n /** Backend harness type, such as `opencode`; the runtime default when omitted. */\n harness?: string\n store: HostedAgentStore\n /** Answers per person per UTC day before `allow` must decide. Default 20. */\n freeTurnsPerDay?: number\n /**\n * Decide a new message after the free allowance is used, for example reply\n * with a checkout link. Without it, a person past the allowance 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 /** 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\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 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\nconst TWO_DAYS = 2 * 86_400\nconst RESUMABLE = new Set(['stopped', 'expired'])\nconst GONE = new Set(['failed', 'deleted'])\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)))\nasync function hmac(secret: string, value: string): Promise<string> {\n const key = await crypto.subtle.importKey('raw', encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])\n return hex(await crypto.subtle.sign('HMAC', key, encode(value)))\n}\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 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\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 const hub = new HubClient({ baseUrl: config.hubUrl ?? 'https://id.tangle.tools', apiKey: config.apiKey })\n const backend: BackendConfig = { ...(config.harness ? { type: config.harness as BackendConfig['type'] } : {}), profile: config.profile }\n const profileTag = sha256(JSON.stringify(config.profile)).then(hash => hash.slice(0, 8))\n\n /** The person's running box: created on the first message, resumed later,\n * replaced when the platform deleted it. Null when the deadline passed. */\n async function ensureBox(user: string, deadline: number): Promise<SandboxInstance | null> {\n const key = `box:${user}`\n const known = await store.get(key)\n let box = known ? await sandbox.get(known) : null\n if (!box || GONE.has(box.status)) {\n // The key names the box it replaces, so concurrent first messages and\n // retries converge on one allocation instead of each creating a box.\n box = await sandbox.createIsolated({\n name: `hosted-${user.slice(0, 24)}`, idempotencyKey: known ? `hosted-${user}-after-${known}` : `hosted-${user}`,\n resources: { cpuCores: policy.cpuCores, memoryMB: policy.memoryMB, diskGB: policy.diskGB },\n backend, 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 await store.put(key, box.id)\n }\n if (RESUMABLE.has(box.status)) await box.resume()\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 /** Admit a new message once: STOP/START, the free 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 const dayKey = `turns:${user}:${new Date().toISOString().slice(0, 10)}`\n // KV has no atomic increment, so concurrent messages may overshoot the\n // allowance by a few; the sponsor key's budget is the hard ceiling.\n const used = Number(await store.get(dayKey) ?? 0)\n const decision = used < (config.freeTurnsPerDay ?? 20) ? 'answer'\n : config.allow ? await config.allow(message, used) : { reply: NOTICE.limit }\n if (decision === 'answer') await store.put(dayKey, String(used + 1), { expirationTtl: TWO_DAYS })\n return decision\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 }): 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 // Scoped to the person: a voice ticket is model-supplied, so a turn id\n // admitted for one person must not skip another person's allowance.\n const admittedKey = `turn:${user}:${message.turnId}`\n if (!await store.get(admittedKey)) {\n const decision = await admit(message, user)\n if (decision === 'ignore') return { state: 'declined' }\n if (decision !== 'answer') return { state: 'declined', reply: decision.reply }\n await store.put(admittedKey, '1', { expirationTtl: TWO_DAYS })\n }\n const box = await ensureBox(user, options.deadline)\n if (!box) return { state: 'pending' }\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 // conversation, so the persona always matches the one in force.\n const sessionId = `hosted-${user}-${await profileTag}`\n if (!await box.session(sessionId).status()) await box.createSession({ sessionId, retention: 'workspace', backend })\n const prompt = message.channel === 'voice' ? `[Phone call. Answer in one to three short spoken sentences.]\\n${text}` : text\n for (;;) {\n const result = await box.driveConversationTurn(prompt, { sessionId, turnId: message.turnId, wallCapMs, timeoutMs: 8000 })\n if (result.state === 'completed') {\n const answer = result.text.trim()\n if (!answer) throw new HostedAgentError('empty_reply', 'The turn completed with no reply text.')\n return { state: 'answered', text: answer }\n }\n if (result.state === 'failed') throw new HostedAgentError('turn_failed', result.error)\n if (Date.now() + 2000 > options.deadline) return { state: 'pending' }\n await sleep(2000)\n }\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 let text: string\n try {\n const result = await ask({ userId, channel: 'imessage', text: event.text, turnId },\n { deadline: Date.now() + wallCapMs + 60_000 })\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 } 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 const claims = b64(JSON.stringify({ phone, exp: Date.now() + 2 * 3_600_000 }))\n return json({ admit: true, callToken: `hvt.${claims}.${await hmac(secret, claims)}` })\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 [tag, claims, signature] = (request.headers.get('x-voice-call-token') ?? '').split('.')\n if (tag !== 'hvt' || !claims || !await equal(signature, await hmac(secret, claims))) return json({ status: 'error', error: 'call_not_admitted' }, 403)\n const call = JSON.parse(unb64(claims)) 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 const result = await ask({ userId: call.phone, channel: 'voice', text: unb64(asked), turnId }, { deadline: Date.now() + (config.voiceBudgetMs ?? 8000) })\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"],"mappings":"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","names":[]}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MembersPanel
|
|
3
|
+
} from "../chunk-GRDLGQ6A.js";
|
|
1
4
|
import {
|
|
2
5
|
InvitationsPanel
|
|
3
6
|
} from "../chunk-5MI2SKQH.js";
|
|
7
|
+
import "../chunk-AULM4X7U.js";
|
|
4
8
|
import {
|
|
5
9
|
InviteAcceptPage
|
|
6
10
|
} from "../chunk-W7P5KV6O.js";
|
|
7
|
-
import {
|
|
8
|
-
MembersPanel
|
|
9
|
-
} from "../chunk-GRDLGQ6A.js";
|
|
10
|
-
import "../chunk-AULM4X7U.js";
|
|
11
11
|
import "../chunk-6XIAPIW6.js";
|
|
12
12
|
export {
|
|
13
13
|
InvitationsPanel,
|
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.5",
|
|
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": [
|
|
@@ -212,6 +212,11 @@
|
|
|
212
212
|
"import": "./dist/integrations/index.js",
|
|
213
213
|
"default": "./dist/integrations/index.js"
|
|
214
214
|
},
|
|
215
|
+
"./hosted-agent": {
|
|
216
|
+
"types": "./dist/hosted-agent/index.d.ts",
|
|
217
|
+
"import": "./dist/hosted-agent/index.js",
|
|
218
|
+
"default": "./dist/hosted-agent/index.js"
|
|
219
|
+
},
|
|
215
220
|
"./interactions": {
|
|
216
221
|
"types": "./dist/interactions/index.d.ts",
|
|
217
222
|
"import": "./dist/interactions/index.js",
|
|
@@ -543,6 +548,7 @@
|
|
|
543
548
|
"@tangle-network/agent-profile-materialize": "0.20.1",
|
|
544
549
|
"@tangle-network/agent-runtime": "0.249.1",
|
|
545
550
|
"@tangle-network/brand": "1.5.0",
|
|
551
|
+
"@tangle-network/hub-sdk": "0.14.2",
|
|
546
552
|
"@tangle-network/sandbox": "0.45.0",
|
|
547
553
|
"@tangle-network/sandbox-ui": "0.113.3",
|
|
548
554
|
"@tangle-network/ui": "11.9.0",
|
|
@@ -603,6 +609,7 @@
|
|
|
603
609
|
"@tangle-network/agent-profile-materialize": ">=0.20.1 <0.21.0",
|
|
604
610
|
"@tangle-network/agent-runtime": ">=0.249.1 <0.250.0",
|
|
605
611
|
"@tangle-network/brand": ">=1.5.0",
|
|
612
|
+
"@tangle-network/hub-sdk": ">=0.12.6 <0.15.0",
|
|
606
613
|
"@tangle-network/sandbox": ">=0.45.0 <0.46.0",
|
|
607
614
|
"@tangle-network/sandbox-ui": ">=0.113.3 <0.114.0",
|
|
608
615
|
"@tangle-network/ui": ">=11.6.0 <12.0.0",
|
|
@@ -702,6 +709,9 @@
|
|
|
702
709
|
},
|
|
703
710
|
"react-dom": {
|
|
704
711
|
"optional": true
|
|
712
|
+
},
|
|
713
|
+
"@tangle-network/hub-sdk": {
|
|
714
|
+
"optional": true
|
|
705
715
|
}
|
|
706
716
|
},
|
|
707
717
|
"dependencies": {
|