@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.
@@ -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
- createKey(input: Parameters<KeyProvisioner['createKey']>[0]): Promise<DurableWorkspaceKeyCreateResult>;
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
- /** Keep a failed cleanup visible and schedule a later retry. */
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
- /** Retry due cleanup rows. A supplied owner/workspace limits the retry to its scope. */
143
- retryPendingRevocations(scope?: Pick<WorkspaceKeyIdentity, 'ownerUserId' | 'workspaceId'>): Promise<number>;
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;
@@ -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';
@@ -3,7 +3,7 @@ import {
3
3
  createPlatformBalanceManager,
4
4
  createTcloudKeyProvisioner,
5
5
  createWorkspaceKeyManager
6
- } from "../chunk-TS2ZIB53.js";
6
+ } from "../chunk-4ZJYNI53.js";
7
7
  export {
8
8
  createIdentityBoundWorkspaceKeyManager,
9
9
  createPlatformBalanceManager,
@@ -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,