@tangle-network/agent-app 0.47.5 → 0.47.7
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/billing/identity-bound.d.ts +48 -4
- package/dist/billing/index.d.ts +1 -1
- package/dist/billing/index.js +1 -1
- package/dist/chat-routes/index.d.ts +1 -0
- package/dist/chat-routes/index.js +182 -0
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-routes/protected-runtime-producer.d.ts +22 -0
- package/dist/{chunk-TS2ZIB53.js → chunk-4ZJYNI53.js} +112 -30
- package/dist/chunk-4ZJYNI53.js.map +1 -0
- package/dist/chunk-KHRIPDW5.js +135 -0
- package/dist/chunk-KHRIPDW5.js.map +1 -0
- package/dist/preset-cloudflare/index.js +1 -1
- package/dist/runtime/index.d.ts +1 -0
- package/dist/runtime/index.js +6 -0
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/protected-model.d.ts +29 -0
- package/package.json +1 -1
- package/dist/chunk-TS2ZIB53.js.map +0 -1
|
@@ -29,6 +29,8 @@ export interface DurableWorkspaceKeyRecord {
|
|
|
29
29
|
sourceKeyFingerprint: string;
|
|
30
30
|
/** The persisted name used to recover a remote create after a crash. Null only for pre-name rows. */
|
|
31
31
|
name: string | null;
|
|
32
|
+
/** The persisted retry identity for the remote create. Absent on legacy rows without a persisted create identity. */
|
|
33
|
+
idempotencyKey?: string | null;
|
|
32
34
|
keyId: string;
|
|
33
35
|
keyEncrypted: string;
|
|
34
36
|
budgetUsd: number;
|
|
@@ -48,9 +50,21 @@ type DurableWorkspaceKeyCreateResult = Awaited<ReturnType<KeyProvisioner['create
|
|
|
48
50
|
budgetRemaining?: number;
|
|
49
51
|
expiresAt?: string | null;
|
|
50
52
|
};
|
|
53
|
+
/** The exact request identity that must be reused when recovering a create. */
|
|
54
|
+
export interface DurableWorkspaceKeyCreateInput {
|
|
55
|
+
name: string;
|
|
56
|
+
product: string;
|
|
57
|
+
budgetUsd: number;
|
|
58
|
+
expiresAt: string;
|
|
59
|
+
/** Stable across process restarts for one persisted provisioning row. */
|
|
60
|
+
idempotencyKey: string;
|
|
61
|
+
}
|
|
51
62
|
/** The remote operations required by the durable manager. */
|
|
52
63
|
export interface DurableWorkspaceKeyProvisioner {
|
|
53
|
-
|
|
64
|
+
/** True only when replay returns the original child for the persisted request identity. */
|
|
65
|
+
supportsIdempotentCreate?: true;
|
|
66
|
+
/** Create one child key. Non-idempotent providers recover through stable-name discovery only. */
|
|
67
|
+
createKey(input: DurableWorkspaceKeyCreateInput): Promise<DurableWorkspaceKeyCreateResult>;
|
|
54
68
|
getKey(id: string): Promise<{
|
|
55
69
|
budgetUsd?: number | null;
|
|
56
70
|
budgetSpent?: number;
|
|
@@ -66,6 +80,22 @@ export interface DurableWorkspaceKeyProvisioner {
|
|
|
66
80
|
id: string;
|
|
67
81
|
}>>;
|
|
68
82
|
}
|
|
83
|
+
/** Optional compare-and-set lifecycle writes for stores that support fencing. */
|
|
84
|
+
export interface DurableWorkspaceKeyConditionalWrites {
|
|
85
|
+
/** Return false when the row was no longer provisioning. */
|
|
86
|
+
markProvisioningRemote(input: {
|
|
87
|
+
id: string;
|
|
88
|
+
keyId: string;
|
|
89
|
+
}): Promise<boolean>;
|
|
90
|
+
/** Return false when the row was no longer provisioning. */
|
|
91
|
+
markActive(input: {
|
|
92
|
+
id: string;
|
|
93
|
+
keyId: string;
|
|
94
|
+
keyEncrypted: string;
|
|
95
|
+
expiresAt: Date;
|
|
96
|
+
budgetUsd: number;
|
|
97
|
+
}): Promise<boolean>;
|
|
98
|
+
}
|
|
69
99
|
/** Persistence operations for identity-bound key lifecycle state. */
|
|
70
100
|
export interface DurableWorkspaceKeyStore {
|
|
71
101
|
/** Return the active row for this exact product scope. */
|
|
@@ -87,7 +117,12 @@ export interface DurableWorkspaceKeyStore {
|
|
|
87
117
|
expiresAt: Date;
|
|
88
118
|
budgetUsd: number;
|
|
89
119
|
}): Promise<void>;
|
|
90
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Optional fenced writes. The manager uses false to compensate a stale
|
|
122
|
+
* creator. Stores without this field retain the pre-fence behavior.
|
|
123
|
+
*/
|
|
124
|
+
conditionalWrites?: DurableWorkspaceKeyConditionalWrites;
|
|
125
|
+
/** Keep a failed cleanup visible and schedule a later retry. Terminal rows are immutable. */
|
|
91
126
|
markRevocationPending(input: {
|
|
92
127
|
id: string;
|
|
93
128
|
error?: string | null;
|
|
@@ -139,13 +174,22 @@ export interface DurableWorkspaceKeyManager {
|
|
|
139
174
|
}): Promise<WorkspaceRuntimeKey>;
|
|
140
175
|
/** Read usage without returning the child secret. */
|
|
141
176
|
getUsage(identity: WorkspaceKeyIdentity): Promise<WorkspaceKeyUsage | null>;
|
|
142
|
-
/**
|
|
143
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Retry due cleanup rows. A supplied owner/workspace limits the retry to its
|
|
179
|
+
* scope. Empty provisioning rows also require the full identity so a retry
|
|
180
|
+
* can use the original source binding safely.
|
|
181
|
+
*/
|
|
182
|
+
retryPendingRevocations(scope?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>, identity?: WorkspaceKeyIdentity): Promise<number>;
|
|
144
183
|
}
|
|
145
184
|
/** Configuration for {@link createIdentityBoundWorkspaceKeyManager}. */
|
|
146
185
|
export interface DurableWorkspaceKeyManagerOptions {
|
|
147
186
|
store: DurableWorkspaceKeyStore;
|
|
148
187
|
provisioner: DurableWorkspaceKeyProvisioner;
|
|
188
|
+
/**
|
|
189
|
+
* Control-plane client for historical get/revoke/list operations. Use this
|
|
190
|
+
* when the source credential can rotate or disappear. It never mints keys.
|
|
191
|
+
*/
|
|
192
|
+
recoveryProvisioner?: Pick<DurableWorkspaceKeyProvisioner, 'getKey' | 'revokeKey' | 'findCreatedKeys'>;
|
|
149
193
|
crypto: KeyCrypto;
|
|
150
194
|
/** Product partition. Products must use separate values. */
|
|
151
195
|
product: WorkspaceKeyProduct;
|
package/dist/billing/index.d.ts
CHANGED
|
@@ -239,4 +239,4 @@ export interface PlatformBalanceManager<Plan extends string> {
|
|
|
239
239
|
export declare function createPlatformBalanceManager<Plan extends string>(opts: PlatformBalanceManagerOptions<Plan>): PlatformBalanceManager<Plan>;
|
|
240
240
|
/** Create a workspace key manager that handles key provisioning and budget tracking */
|
|
241
241
|
export declare function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager;
|
|
242
|
-
export { createIdentityBoundWorkspaceKeyManager, type DurableWorkspaceKeyManager, type DurableWorkspaceKeyManagerOptions, type DurableWorkspaceKeyProvisioner, type DurableWorkspaceKeyProvisioningRecord, type DurableWorkspaceKeyRecord, type DurableWorkspaceKeyScope, type DurableWorkspaceKeyStore, type WorkspaceKeyIdentity, type WorkspaceKeyProduct, type WorkspaceKeyStatus, type WorkspaceKeyUsage, type WorkspaceRuntimeKey, } from './identity-bound';
|
|
242
|
+
export { createIdentityBoundWorkspaceKeyManager, type DurableWorkspaceKeyManager, type DurableWorkspaceKeyManagerOptions, type DurableWorkspaceKeyCreateInput, type DurableWorkspaceKeyConditionalWrites, type DurableWorkspaceKeyProvisioner, type DurableWorkspaceKeyProvisioningRecord, type DurableWorkspaceKeyRecord, type DurableWorkspaceKeyScope, type DurableWorkspaceKeyStore, type WorkspaceKeyIdentity, type WorkspaceKeyProduct, type WorkspaceKeyStatus, type WorkspaceKeyUsage, type WorkspaceRuntimeKey, } from './identity-bound';
|
package/dist/billing/index.js
CHANGED
|
@@ -13,6 +13,7 @@ export * from './turn-routes';
|
|
|
13
13
|
export * from './gateway-adapter';
|
|
14
14
|
export * from './stale-turn-lock';
|
|
15
15
|
export * from './sandbox-producer';
|
|
16
|
+
export * from './protected-runtime-producer';
|
|
16
17
|
export * from './model-failover-stream';
|
|
17
18
|
export * from './draft-persistence';
|
|
18
19
|
export * from './detached-turn';
|
|
@@ -49,6 +49,9 @@ import "../chunk-KBJL6LIW.js";
|
|
|
49
49
|
import "../chunk-TX6S7XXU.js";
|
|
50
50
|
import "../chunk-UGWQLQDS.js";
|
|
51
51
|
import "../chunk-S5SRJJQG.js";
|
|
52
|
+
import {
|
|
53
|
+
ProtectedModelSettlementError
|
|
54
|
+
} from "../chunk-KHRIPDW5.js";
|
|
52
55
|
import "../chunk-JML7WKWU.js";
|
|
53
56
|
import {
|
|
54
57
|
assistantRowIdForTurn,
|
|
@@ -1037,6 +1040,184 @@ ${diagnostic.userMessage}` : diagnostic.userMessage;
|
|
|
1037
1040
|
};
|
|
1038
1041
|
}
|
|
1039
1042
|
|
|
1043
|
+
// src/chat-routes/protected-runtime-producer.ts
|
|
1044
|
+
import { createExecutor, streamAgentTurn } from "@tangle-network/agent-runtime/kernel";
|
|
1045
|
+
import {
|
|
1046
|
+
runProtectedAgentCandidateModelGrant
|
|
1047
|
+
} from "@tangle-network/agent-runtime/candidate-execution";
|
|
1048
|
+
function createProtectedRuntimeChatProducer(options) {
|
|
1049
|
+
if (!Number.isSafeInteger(options.maxToolCalls) || options.maxToolCalls < 0) {
|
|
1050
|
+
throw new Error("maxToolCalls must be a non-negative integer");
|
|
1051
|
+
}
|
|
1052
|
+
options = {
|
|
1053
|
+
...options,
|
|
1054
|
+
grant: { ...options.grant, resolve: structuredClone(options.grant.resolve), reserve: structuredClone(options.grant.reserve) },
|
|
1055
|
+
tools: options.tools.map((tool) => ({ ...tool, inputSchema: structuredClone(tool.inputSchema) })),
|
|
1056
|
+
priorMessages: options.priorMessages ? structuredClone(options.priorMessages) : void 0
|
|
1057
|
+
};
|
|
1058
|
+
const tools = new Map(options.tools.map((tool) => [tool.name, tool]));
|
|
1059
|
+
if (tools.size !== options.tools.length) throw new Error("Duplicate protected tool name");
|
|
1060
|
+
const profile = structuredClone(options.profile);
|
|
1061
|
+
let toolCalls = 0;
|
|
1062
|
+
let usage = {};
|
|
1063
|
+
let started = false;
|
|
1064
|
+
let model;
|
|
1065
|
+
let budgetEnforced = false;
|
|
1066
|
+
let toolTokens;
|
|
1067
|
+
async function* events() {
|
|
1068
|
+
if (started) throw new Error("A protected chat producer can only execute once");
|
|
1069
|
+
started = true;
|
|
1070
|
+
const controller = new AbortController();
|
|
1071
|
+
options.signal?.throwIfAborted();
|
|
1072
|
+
if (Date.now() >= options.grant.deadlineAtMs) throw new Error("Protected execution deadline expired");
|
|
1073
|
+
const signal = AbortSignal.any([
|
|
1074
|
+
controller.signal,
|
|
1075
|
+
AbortSignal.timeout(Math.max(1, options.grant.deadlineAtMs - Date.now())),
|
|
1076
|
+
...options.signal ? [options.signal] : []
|
|
1077
|
+
]);
|
|
1078
|
+
const channel = new TransformStream();
|
|
1079
|
+
const writer = channel.writable.getWriter();
|
|
1080
|
+
const reader = channel.readable.getReader();
|
|
1081
|
+
const emit = (event) => writer.write(event);
|
|
1082
|
+
const execution = runProtectedAgentCandidateModelGrant({
|
|
1083
|
+
...options.grant,
|
|
1084
|
+
port: {
|
|
1085
|
+
resolve: (input) => options.grant.port.resolve(input),
|
|
1086
|
+
reserveGrant: (input) => options.grant.port.reserveGrant(input),
|
|
1087
|
+
activateGrant: (input) => options.grant.port.activateGrant(input),
|
|
1088
|
+
// Runtime settles failed executions before rethrowing. Retain that receipt too.
|
|
1089
|
+
settleGrant: async (input) => {
|
|
1090
|
+
let auditError;
|
|
1091
|
+
const settlement = await options.grant.port.settleGrant(input).catch((error) => {
|
|
1092
|
+
if (!(error instanceof ProtectedModelSettlementError)) throw error;
|
|
1093
|
+
auditError = error;
|
|
1094
|
+
return error.settlement;
|
|
1095
|
+
});
|
|
1096
|
+
usage = {
|
|
1097
|
+
inputTokens: settlement.calls.reduce((sum, call) => sum + call.accountedInputTokens, 0),
|
|
1098
|
+
outputTokens: settlement.calls.reduce((sum, call) => sum + call.outputTokens, 0),
|
|
1099
|
+
reasoningTokens: settlement.calls.reduce((sum, call) => sum + call.reasoningTokens, 0),
|
|
1100
|
+
costUsd: settlement.calls.reduce((sum, call) => sum + call.costUsdNanos, 0) / 1e9
|
|
1101
|
+
};
|
|
1102
|
+
budgetEnforced = settlement.usageWithinLimits;
|
|
1103
|
+
if (!budgetEnforced) throw new Error("Provider settlement exceeded execution limits");
|
|
1104
|
+
if (auditError) throw auditError;
|
|
1105
|
+
return settlement;
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
execute: async ({ activation, resolved }) => {
|
|
1109
|
+
signal.throwIfAborted();
|
|
1110
|
+
if (Date.now() >= options.grant.deadlineAtMs) throw new Error("Protected execution deadline expired");
|
|
1111
|
+
model = resolved.model;
|
|
1112
|
+
if (resolved.provider !== "anthropic" || resolved.reasoningEffort !== "none") {
|
|
1113
|
+
throw new Error("Protected chat currently requires Anthropic without reasoning");
|
|
1114
|
+
}
|
|
1115
|
+
toolTokens = 0;
|
|
1116
|
+
const apiKey = activation.env.OPENAI_API_KEY;
|
|
1117
|
+
const baseUrl = activation.env.OPENAI_BASE_URL;
|
|
1118
|
+
if (!apiKey || !baseUrl) throw new Error("Protected Router activation is missing");
|
|
1119
|
+
const factory = createExecutor({
|
|
1120
|
+
backend: "router-tools",
|
|
1121
|
+
routerBaseUrl: baseUrl,
|
|
1122
|
+
routerKey: apiKey,
|
|
1123
|
+
tools: options.tools.map((tool) => ({
|
|
1124
|
+
type: "function",
|
|
1125
|
+
function: { name: tool.name, description: tool.description, parameters: tool.inputSchema }
|
|
1126
|
+
})),
|
|
1127
|
+
...options.priorMessages?.length ? { initialMessages: options.priorMessages } : {},
|
|
1128
|
+
executeToolCall: async (name, args) => {
|
|
1129
|
+
signal.throwIfAborted();
|
|
1130
|
+
const tool = tools.get(name);
|
|
1131
|
+
if (!tool) throw new Error("Tool is not authorized for this execution");
|
|
1132
|
+
if (toolCalls >= options.maxToolCalls) {
|
|
1133
|
+
controller.abort(new Error("Execution tool-call limit reached"));
|
|
1134
|
+
signal.throwIfAborted();
|
|
1135
|
+
}
|
|
1136
|
+
toolCalls += 1;
|
|
1137
|
+
const id = `tool-${toolCalls}`;
|
|
1138
|
+
await emit({ type: "tool_call", data: { id, name, arguments: args } });
|
|
1139
|
+
try {
|
|
1140
|
+
const result = await tool.run(args, { signal });
|
|
1141
|
+
await emit({ type: "tool_result", data: { id, name, output: result } });
|
|
1142
|
+
return JSON.stringify(result);
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
const message = error instanceof Error ? error.message : "Tool failed";
|
|
1145
|
+
await emit({ type: "tool_result", data: { id, name, error: message } });
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
let completed = false;
|
|
1151
|
+
for await (const event of streamAgentTurn(
|
|
1152
|
+
{ kind: "executor", factory, profile },
|
|
1153
|
+
{ prompt: options.prompt },
|
|
1154
|
+
{
|
|
1155
|
+
signal,
|
|
1156
|
+
callId: options.grant.reserve.executionId,
|
|
1157
|
+
timeoutMs: Math.max(1, options.grant.deadlineAtMs - Date.now())
|
|
1158
|
+
}
|
|
1159
|
+
)) {
|
|
1160
|
+
if (event.type === "text_delta" || event.type === "reasoning_delta") {
|
|
1161
|
+
await emit({ type: "message.part.updated", data: {
|
|
1162
|
+
part: { id: event.type, type: event.type === "text_delta" ? "text" : "reasoning" },
|
|
1163
|
+
delta: event.text
|
|
1164
|
+
} });
|
|
1165
|
+
}
|
|
1166
|
+
if (event.type === "final") {
|
|
1167
|
+
if (event.status !== "completed") throw new Error(`Protected execution ${event.status}`);
|
|
1168
|
+
completed = true;
|
|
1169
|
+
await emit({ type: "result", data: { finalText: event.text } });
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
if (!completed) throw new Error("Protected execution ended without a terminal result");
|
|
1173
|
+
}
|
|
1174
|
+
}).then(async () => {
|
|
1175
|
+
await writer.close();
|
|
1176
|
+
}).catch((error) => writer.abort(error));
|
|
1177
|
+
try {
|
|
1178
|
+
for (; ; ) {
|
|
1179
|
+
const next = await reader.read();
|
|
1180
|
+
if (next.done) break;
|
|
1181
|
+
yield next.value;
|
|
1182
|
+
}
|
|
1183
|
+
await execution;
|
|
1184
|
+
} finally {
|
|
1185
|
+
controller.abort();
|
|
1186
|
+
await reader.cancel().catch(() => void 0);
|
|
1187
|
+
await execution;
|
|
1188
|
+
reader.releaseLock();
|
|
1189
|
+
writer.releaseLock();
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const producer = createSandboxChatProducer({ events: events(), model: profile.model?.default });
|
|
1193
|
+
return {
|
|
1194
|
+
...producer,
|
|
1195
|
+
stream: (async function* () {
|
|
1196
|
+
let failure;
|
|
1197
|
+
for await (const event of producer.stream) {
|
|
1198
|
+
if (event.type === "error") failure = event;
|
|
1199
|
+
else yield event;
|
|
1200
|
+
}
|
|
1201
|
+
if (usage.inputTokens !== void 0 && usage.outputTokens !== void 0 && usage.costUsd !== void 0) {
|
|
1202
|
+
yield { type: "usage", usage: {
|
|
1203
|
+
promptTokens: usage.inputTokens,
|
|
1204
|
+
completionTokens: usage.outputTokens,
|
|
1205
|
+
providerCostUsd: usage.costUsd,
|
|
1206
|
+
reasoningTokens: usage.reasoningTokens,
|
|
1207
|
+
toolTokens,
|
|
1208
|
+
toolCallCount: toolCalls,
|
|
1209
|
+
budgetEnforced
|
|
1210
|
+
} };
|
|
1211
|
+
}
|
|
1212
|
+
if (failure) yield failure;
|
|
1213
|
+
})(),
|
|
1214
|
+
get model() {
|
|
1215
|
+
return model ?? profile.model?.default;
|
|
1216
|
+
},
|
|
1217
|
+
usage: () => ({ ...usage })
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1040
1221
|
// src/chat-routes/detached-turn.ts
|
|
1041
1222
|
var TERMINAL_ERROR_TYPES = /* @__PURE__ */ new Set(["error", "session.run.failed"]);
|
|
1042
1223
|
function errorMessageOf(ev) {
|
|
@@ -2583,6 +2764,7 @@ export {
|
|
|
2583
2764
|
createAtomicAttachmentWriter,
|
|
2584
2765
|
createAttachmentUploadRoute,
|
|
2585
2766
|
createChatTurnRoutes,
|
|
2767
|
+
createProtectedRuntimeChatProducer,
|
|
2586
2768
|
createSandboxChatProducer,
|
|
2587
2769
|
createSandboxFileIndexRoute,
|
|
2588
2770
|
createUploadRoute,
|