@tangle-network/agent-app 0.43.76 → 0.44.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DesignCanvas-Z362I3R3.js +10 -0
- package/dist/{DesignCanvasEditor-FJIVYOA2.js → DesignCanvasEditor-ZFE2W4OJ.js} +4 -4
- package/dist/assets/index.d.ts +4 -4
- package/dist/assets/index.js +223 -11
- package/dist/assets/index.js.map +1 -1
- package/dist/assistant/index.d.ts +7 -7
- package/dist/assistant/index.js +3 -3
- package/dist/assistant/index.js.map +1 -1
- package/dist/{attachment-validation-Dv1A_Puy.d.ts → attachment-validation-DRuLEjAE.d.ts} +1 -1
- package/dist/{auth-_FU8w01b.d.ts → auth-anc7mv2W.d.ts} +2 -2
- package/dist/chat-routes/index.d.ts +14 -11
- package/dist/chat-routes/index.js +16 -17
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-store/index.d.ts +26 -7
- package/dist/chat-store/index.js +14 -5
- package/dist/chat-store/index.js.map +1 -1
- package/dist/{chunk-ROLFDOMJ.js → chunk-4TI7QWLW.js} +3 -3
- package/dist/{chunk-3JUTOVUH.js → chunk-6KMAM7BL.js} +1 -1
- package/dist/chunk-6KMAM7BL.js.map +1 -0
- package/dist/{chunk-WL7XHLDK.js → chunk-ICOHEZK6.js} +77 -5
- package/dist/chunk-ICOHEZK6.js.map +1 -0
- package/dist/{chunk-22GLESKE.js → chunk-O63WWYET.js} +2 -4
- package/dist/chunk-O63WWYET.js.map +1 -0
- package/dist/{chunk-E7N67FYI.js → chunk-Q3FONVAK.js} +5 -5
- package/dist/{chunk-E7N67FYI.js.map → chunk-Q3FONVAK.js.map} +1 -1
- package/dist/{chunk-RXOTWZ4G.js → chunk-TKRI463Z.js} +4 -4
- package/dist/chunk-TKRI463Z.js.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +123 -4
- package/dist/config/index.js.map +1 -1
- package/dist/{contract-B3h7peV3.d.ts → contract-CEewO6DI.d.ts} +1 -1
- package/dist/design-canvas/index.d.ts +4 -4
- package/dist/design-canvas-react/index.js +7 -7
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/eval/index.d.ts +1 -1
- package/dist/eval/index.js +22 -8
- package/dist/eval/index.js.map +1 -1
- package/dist/{flow-types-CqomVAUN.d.ts → flow-types-CJxEmaRy.d.ts} +1 -1
- package/dist/integrations/index.js +67 -5
- package/dist/integrations/index.js.map +1 -1
- package/dist/interactions/index.d.ts +6 -4
- package/dist/interactions/index.js +1 -2
- package/dist/knowledge/index.js +63 -4
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge-loop/index.d.ts +1 -1
- package/dist/knowledge-loop/index.js +94 -4
- package/dist/knowledge-loop/index.js.map +1 -1
- package/dist/{mcp-BjPqQ-3C.d.ts → mcp-D3qVzbE1.d.ts} +2 -2
- package/dist/{mcp-rpc-BfUOsalw.d.ts → mcp-rpc-CzU5LWWT.d.ts} +1 -1
- package/dist/missions/index.js +1143 -23
- package/dist/missions/index.js.map +1 -1
- package/dist/{model-DmdkIteM.d.ts → model-CdCDfBA9.d.ts} +1 -1
- package/dist/object-store/index.js +113 -8
- package/dist/object-store/index.js.map +1 -1
- package/dist/{parts-2ymE5cs-.d.ts → parts-Co_lFgLI.d.ts} +3 -3
- package/dist/platform/index.d.ts +1 -1
- package/dist/preflight/cli.d.ts +13 -0
- package/dist/preflight/cli.js +50 -0
- package/dist/preflight/cli.js.map +1 -0
- package/dist/preset-cloudflare/index.d.ts +1 -1
- package/dist/preset-cloudflare/index.js +284 -11
- package/dist/preset-cloudflare/index.js.map +1 -1
- package/dist/{queue-C24V13h9.d.ts → queue-CD5-_VTX.d.ts} +1 -1
- package/dist/redact/index.js +116 -8
- package/dist/redact/index.js.map +1 -1
- package/dist/runtime/index.d.ts +19 -111
- package/dist/runtime/index.js +234 -14
- package/dist/runtime/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +3 -3
- package/dist/sandbox/index.js +2 -3
- package/dist/sequences/index.d.ts +4 -4
- package/dist/stream/index.d.ts +4 -4
- package/dist/{stream-normalizer-QYUl4vnl.d.ts → stream-normalizer-DnuqkZvw.d.ts} +2 -2
- package/dist/tangle/index.js +42 -4
- package/dist/tangle/index.js.map +1 -1
- package/dist/teams/index.js +5 -5
- package/dist/teams/invitations-api.js +4 -4
- package/dist/teams-react/index.js +3 -3
- package/dist/theme/index.js +0 -1
- package/dist/tools/index.d.ts +6 -6
- package/dist/tools/index.js +5 -7
- package/dist/trace/index.d.ts +1 -1
- package/dist/trace/index.js +179 -10
- package/dist/trace/index.js.map +1 -1
- package/dist/{turn-buffer-BaSgxVmK.d.ts → turn-buffer-BcPfhr1_.d.ts} +1 -1
- package/dist/turn-stream/index.d.ts +1 -1
- package/dist/{types-CBRyqijY.d.ts → types-BCxK0wyS.d.ts} +1 -1
- package/dist/{types-CEchbvgz.d.ts → types-XIZqIdX1.d.ts} +1 -1
- package/dist/web-react/index.d.ts +10 -10
- package/dist/web-react/index.js +5 -5
- package/dist/work-product/index.d.ts +4 -4
- package/dist/work-product/index.js +735 -15
- package/dist/work-product/index.js.map +1 -1
- package/dist/work-product-react/index.d.ts +1 -1
- package/dist/work-product-react/index.js.map +1 -1
- package/package.json +4 -19
- package/dist/DesignCanvas-UNL34LS2.js +0 -12
- package/dist/chunk-22GLESKE.js.map +0 -1
- package/dist/chunk-3JUTOVUH.js.map +0 -1
- package/dist/chunk-5EQCITY3.js +0 -20
- package/dist/chunk-5EQCITY3.js.map +0 -1
- package/dist/chunk-5SD6HT2V.js +0 -236
- package/dist/chunk-5SD6HT2V.js.map +0 -1
- package/dist/chunk-5TESA4SN.js +0 -74
- package/dist/chunk-5TESA4SN.js.map +0 -1
- package/dist/chunk-6GQ5DKFK.js +0 -920
- package/dist/chunk-6GQ5DKFK.js.map +0 -1
- package/dist/chunk-7ESQUSAC.js +0 -343
- package/dist/chunk-7ESQUSAC.js.map +0 -1
- package/dist/chunk-7LNGJDNA.js +0 -1
- package/dist/chunk-7LNGJDNA.js.map +0 -1
- package/dist/chunk-AP2UXKUK.js +0 -69
- package/dist/chunk-AP2UXKUK.js.map +0 -1
- package/dist/chunk-FA4XR66Y.js +0 -32
- package/dist/chunk-FA4XR66Y.js.map +0 -1
- package/dist/chunk-FKCB5FTD.js +0 -126
- package/dist/chunk-FKCB5FTD.js.map +0 -1
- package/dist/chunk-GPBRL2XR.js +0 -102
- package/dist/chunk-GPBRL2XR.js.map +0 -1
- package/dist/chunk-HRH7ASAG.js +0 -759
- package/dist/chunk-HRH7ASAG.js.map +0 -1
- package/dist/chunk-HYTIHYNV.js +0 -297
- package/dist/chunk-HYTIHYNV.js.map +0 -1
- package/dist/chunk-LGBO24FL.js +0 -190
- package/dist/chunk-LGBO24FL.js.map +0 -1
- package/dist/chunk-NLSSFLLS.js +0 -1168
- package/dist/chunk-NLSSFLLS.js.map +0 -1
- package/dist/chunk-OSMNYPOG.js +0 -129
- package/dist/chunk-OSMNYPOG.js.map +0 -1
- package/dist/chunk-QYOD3K56.js +0 -124
- package/dist/chunk-QYOD3K56.js.map +0 -1
- package/dist/chunk-RXOTWZ4G.js.map +0 -1
- package/dist/chunk-UAKFCMWK.js +0 -1
- package/dist/chunk-UAKFCMWK.js.map +0 -1
- package/dist/chunk-UKTYKG57.js +0 -48
- package/dist/chunk-UKTYKG57.js.map +0 -1
- package/dist/chunk-UOAYS72M.js +0 -80
- package/dist/chunk-UOAYS72M.js.map +0 -1
- package/dist/chunk-WL7XHLDK.js.map +0 -1
- package/dist/composer/index.d.ts +0 -2
- package/dist/composer/index.js +0 -22
- package/dist/composer/index.js.map +0 -1
- package/dist/core-7qIM7svy.d.ts +0 -21
- package/dist/durable-chat/index.d.ts +0 -464
- package/dist/durable-chat/index.js +0 -59
- package/dist/durable-chat/index.js.map +0 -1
- package/dist/index.d.ts +0 -51
- package/dist/index.js +0 -827
- package/dist/index.js.map +0 -1
- /package/dist/{DesignCanvas-UNL34LS2.js.map → DesignCanvas-Z362I3R3.js.map} +0 -0
- /package/dist/{DesignCanvasEditor-FJIVYOA2.js.map → DesignCanvasEditor-ZFE2W4OJ.js.map} +0 -0
- /package/dist/{chunk-ROLFDOMJ.js.map → chunk-4TI7QWLW.js.map} +0 -0
package/dist/chunk-6GQ5DKFK.js
DELETED
|
@@ -1,920 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
cancelStatusFor,
|
|
3
|
-
interactionFromWireRequest,
|
|
4
|
-
interactionToPersistedPart,
|
|
5
|
-
isSafeInteractionFieldKey,
|
|
6
|
-
parseInteractionAnswers,
|
|
7
|
-
parseInteractionCancel,
|
|
8
|
-
parseInteractionRequest,
|
|
9
|
-
questionInteractionContentSignature
|
|
10
|
-
} from "./chunk-3ZK5IJSW.js";
|
|
11
|
-
import {
|
|
12
|
-
canTransitionPlanStatus,
|
|
13
|
-
parsePlanSubmittedEvent,
|
|
14
|
-
persistedPartToPlan,
|
|
15
|
-
planFollowUpTurnId,
|
|
16
|
-
planToPersistedPart
|
|
17
|
-
} from "./chunk-YJMCRXQQ.js";
|
|
18
|
-
|
|
19
|
-
// src/durable-chat/errors.ts
|
|
20
|
-
var DurableChatError = class extends Error {
|
|
21
|
-
code;
|
|
22
|
-
status;
|
|
23
|
-
details;
|
|
24
|
-
constructor(code, message, status, details) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.name = "DurableChatError";
|
|
27
|
-
this.code = code;
|
|
28
|
-
this.status = status;
|
|
29
|
-
this.details = details;
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
var DurableChatConflictError = class extends DurableChatError {
|
|
33
|
-
constructor(message = "durable chat state conflict", details) {
|
|
34
|
-
super("DURABLE_CHAT_CONFLICT", message, 409, details);
|
|
35
|
-
this.name = "DurableChatConflictError";
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
var DurableChatUnavailableError = class extends DurableChatError {
|
|
39
|
-
constructor(message = "durable chat authority unavailable", details) {
|
|
40
|
-
super("DURABLE_CHAT_UNAVAILABLE", message, 503, details);
|
|
41
|
-
this.name = "DurableChatUnavailableError";
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
var DurableChatGoneError = class extends DurableChatError {
|
|
45
|
-
constructor(message = "durable chat plan is gone", details) {
|
|
46
|
-
super("DURABLE_CHAT_GONE", message, 410, details);
|
|
47
|
-
this.name = "DurableChatGoneError";
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// src/durable-chat/types.ts
|
|
52
|
-
function createDurableChatScope(value) {
|
|
53
|
-
if (typeof value !== "string" || value.trim() === "") throw new TypeError("durable chat scope must be a non-empty string");
|
|
54
|
-
return value;
|
|
55
|
-
}
|
|
56
|
-
function durableChatScopeKey(scope) {
|
|
57
|
-
if (typeof scope !== "string" || scope.length === 0) throw new TypeError("durable chat scope is required");
|
|
58
|
-
return scope;
|
|
59
|
-
}
|
|
60
|
-
function normalizePlanDecision(value) {
|
|
61
|
-
if (value === "approved" || value === "approve") return "approved";
|
|
62
|
-
if (value === "rejected" || value === "reject") return "rejected";
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
function planCommandKey(planId, revision, decision) {
|
|
66
|
-
return `plan:${encodeURIComponent(planId)}:${revision}:${decision}`;
|
|
67
|
-
}
|
|
68
|
-
function planAuthorityIdempotencyKey(scope, planId, revision, decision) {
|
|
69
|
-
return `durable-plan:${encodeURIComponent(durableChatScopeKey(scope))}:${encodeURIComponent(planId)}:${revision}:${decision}`;
|
|
70
|
-
}
|
|
71
|
-
function planEffectKey(scope, planId, revision, decision) {
|
|
72
|
-
return `after-decision:${encodeURIComponent(durableChatScopeKey(scope))}:${encodeURIComponent(planId)}:${revision}:${decision}`;
|
|
73
|
-
}
|
|
74
|
-
function stablePlanReceipt(scope, planId, revision, decision, result) {
|
|
75
|
-
const authorityIdempotencyKey = planAuthorityIdempotencyKey(scope, planId, revision, decision);
|
|
76
|
-
return {
|
|
77
|
-
receiptId: `receipt:${authorityIdempotencyKey}`,
|
|
78
|
-
planId,
|
|
79
|
-
revision,
|
|
80
|
-
decision,
|
|
81
|
-
turnId: result.followUp.turnId,
|
|
82
|
-
state: result.followUp.state,
|
|
83
|
-
authorityIdempotencyKey
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// src/durable-chat/memory.ts
|
|
88
|
-
var InMemoryDurableChatStateStore = class {
|
|
89
|
-
plans = /* @__PURE__ */ new Map();
|
|
90
|
-
currentPlans = /* @__PURE__ */ new Map();
|
|
91
|
-
commands = /* @__PURE__ */ new Map();
|
|
92
|
-
effects = /* @__PURE__ */ new Map();
|
|
93
|
-
interactions = /* @__PURE__ */ new Map();
|
|
94
|
-
interactionSemantic = /* @__PURE__ */ new Map();
|
|
95
|
-
/** Duplicate event ids point at the canonical semantic interaction so all
|
|
96
|
-
* duplicate asks settle through one terminal row/attempt. */
|
|
97
|
-
interactionAliases = /* @__PURE__ */ new Map();
|
|
98
|
-
answerIntents = /* @__PURE__ */ new Map();
|
|
99
|
-
scopePrefix(scope) {
|
|
100
|
-
return `${encodeURIComponent(durableChatScopeKey(scope))}\0`;
|
|
101
|
-
}
|
|
102
|
-
scopedKey(scope, ...parts) {
|
|
103
|
-
return `${this.scopePrefix(scope)}${parts.map((part) => encodeURIComponent(String(part))).join("\0")}`;
|
|
104
|
-
}
|
|
105
|
-
planKey(scope, planId, revision) {
|
|
106
|
-
return this.scopedKey(scope, planId, revision);
|
|
107
|
-
}
|
|
108
|
-
currentKey(scope, planId) {
|
|
109
|
-
return this.scopedKey(scope, planId);
|
|
110
|
-
}
|
|
111
|
-
commandKey(scope, key) {
|
|
112
|
-
return this.scopedKey(scope, key);
|
|
113
|
-
}
|
|
114
|
-
interactionKey(scope, id) {
|
|
115
|
-
return this.scopedKey(scope, id);
|
|
116
|
-
}
|
|
117
|
-
intentKey(scope, key) {
|
|
118
|
-
return this.scopedKey(scope, key);
|
|
119
|
-
}
|
|
120
|
-
effectKey(scope, key) {
|
|
121
|
-
return this.scopedKey(scope, key);
|
|
122
|
-
}
|
|
123
|
-
async getPlanProjection(scope, planId, revision) {
|
|
124
|
-
const current = revision === void 0 ? this.currentPlans.get(this.currentKey(scope, planId)) : void 0;
|
|
125
|
-
const key = revision === void 0 ? current : this.planKey(scope, planId, revision);
|
|
126
|
-
return key ? this.plans.get(key) ?? null : null;
|
|
127
|
-
}
|
|
128
|
-
async listPlanProjections(scope, planId) {
|
|
129
|
-
const prefix = this.scopePrefix(scope);
|
|
130
|
-
return [...this.plans.entries()].filter(([key, projection]) => key.startsWith(prefix) && (!planId || projection.planId === planId)).map(([, projection]) => projection).sort((a, b) => a.revision - b.revision);
|
|
131
|
-
}
|
|
132
|
-
async putPlanProjection(scope, projection) {
|
|
133
|
-
if (!projection.planId || !Number.isInteger(projection.revision) || projection.revision < 1) {
|
|
134
|
-
throw new TypeError("plan projection requires planId and positive integer revision");
|
|
135
|
-
}
|
|
136
|
-
const key = this.planKey(scope, projection.planId, projection.revision);
|
|
137
|
-
const prior = this.plans.get(key);
|
|
138
|
-
if (prior && JSON.stringify(prior) !== JSON.stringify(projection)) {
|
|
139
|
-
if (prior.body !== projection.body || prior.title !== projection.title || prior.submittedAt !== projection.submittedAt) {
|
|
140
|
-
throw new DurableChatConflictError("plan content changed without a new revision");
|
|
141
|
-
}
|
|
142
|
-
if (prior.status === projection.status && !["preparing", "pending"].includes(prior.status)) {
|
|
143
|
-
throw new DurableChatConflictError("terminal plan projection cannot be rewritten");
|
|
144
|
-
}
|
|
145
|
-
if (!canTransitionPlanStatus(prior.status, projection.status)) {
|
|
146
|
-
throw new DurableChatConflictError("conflicting plan projection for the same revision");
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
this.plans.set(key, projection);
|
|
150
|
-
const currentKey = this.currentKey(scope, projection.planId);
|
|
151
|
-
const currentKeyValue = this.currentPlans.get(currentKey);
|
|
152
|
-
const current = currentKeyValue ? this.plans.get(currentKeyValue) : void 0;
|
|
153
|
-
if (!current || projection.revision > current.revision) this.currentPlans.set(currentKey, key);
|
|
154
|
-
}
|
|
155
|
-
async getPlanCommand(scope, commandKey) {
|
|
156
|
-
return this.commands.get(this.commandKey(scope, commandKey)) ?? null;
|
|
157
|
-
}
|
|
158
|
-
async claimPlanCommand(scope, command) {
|
|
159
|
-
const key = this.commandKey(scope, command.commandKey);
|
|
160
|
-
const existing = this.commands.get(key);
|
|
161
|
-
if (!existing) {
|
|
162
|
-
const prefix = this.scopePrefix(scope);
|
|
163
|
-
const competing = [...this.commands.entries()].find(
|
|
164
|
-
([storedKey, candidate]) => storedKey.startsWith(prefix) && candidate.planId === command.planId && candidate.revision === command.revision && candidate.decision !== command.decision
|
|
165
|
-
);
|
|
166
|
-
if (competing) {
|
|
167
|
-
return { status: "conflict", record: competing[1], reason: "competing decision for plan revision" };
|
|
168
|
-
}
|
|
169
|
-
this.commands.set(key, command);
|
|
170
|
-
return { status: "claimed", record: command };
|
|
171
|
-
}
|
|
172
|
-
if (existing.planId === command.planId && existing.revision === command.revision && existing.decision === command.decision) return { status: "existing", record: existing };
|
|
173
|
-
return { status: "conflict", record: existing, reason: "command key is already used by another decision" };
|
|
174
|
-
}
|
|
175
|
-
async recordPlanAuthorityResult(scope, commandKey, result, receipt) {
|
|
176
|
-
const key = this.commandKey(scope, commandKey);
|
|
177
|
-
const command = this.commands.get(key);
|
|
178
|
-
if (!command) throw new DurableChatConflictError("cannot record authority result before claiming command");
|
|
179
|
-
command.authorityResult = result;
|
|
180
|
-
command.receipt = receipt;
|
|
181
|
-
command.state = "authority_committed";
|
|
182
|
-
}
|
|
183
|
-
async finalizePlanCommand(scope, commandKey) {
|
|
184
|
-
const command = this.commands.get(this.commandKey(scope, commandKey));
|
|
185
|
-
if (!command) throw new DurableChatConflictError("cannot finalize unknown plan command");
|
|
186
|
-
command.state = "finalized";
|
|
187
|
-
}
|
|
188
|
-
async getPlanEffect(scope, effectKey) {
|
|
189
|
-
return this.effects.get(this.effectKey(scope, effectKey)) ?? null;
|
|
190
|
-
}
|
|
191
|
-
async claimPlanEffect(scope, effect) {
|
|
192
|
-
const key = this.effectKey(scope, effect.effectKey);
|
|
193
|
-
const existing = this.effects.get(key);
|
|
194
|
-
if (existing) return { status: "existing", record: existing };
|
|
195
|
-
this.effects.set(key, effect);
|
|
196
|
-
return { status: "claimed", record: effect };
|
|
197
|
-
}
|
|
198
|
-
async completePlanEffect(scope, effectKey) {
|
|
199
|
-
const effect = this.effects.get(this.effectKey(scope, effectKey));
|
|
200
|
-
if (!effect) throw new DurableChatConflictError("cannot complete unknown plan effect");
|
|
201
|
-
effect.state = "completed";
|
|
202
|
-
effect.completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
203
|
-
}
|
|
204
|
-
async failPlanEffect(scope, effectKey, error) {
|
|
205
|
-
const effect = this.effects.get(this.effectKey(scope, effectKey));
|
|
206
|
-
if (!effect) throw new DurableChatConflictError("cannot fail unknown plan effect");
|
|
207
|
-
effect.state = "error";
|
|
208
|
-
effect.error = error;
|
|
209
|
-
}
|
|
210
|
-
async getInteractionProjection(scope, interactionId) {
|
|
211
|
-
const directKey = this.interactionKey(scope, interactionId);
|
|
212
|
-
const alias = this.interactionAliases.get(directKey);
|
|
213
|
-
return this.interactions.get(alias ?? directKey) ?? null;
|
|
214
|
-
}
|
|
215
|
-
async listInteractionProjections(scope) {
|
|
216
|
-
const prefix = this.scopePrefix(scope);
|
|
217
|
-
return [...this.interactions.entries()].filter(([key]) => key.startsWith(prefix)).map(([, value]) => value);
|
|
218
|
-
}
|
|
219
|
-
async upsertInteractionProjection(scope, projection) {
|
|
220
|
-
if (!projection.id) throw new TypeError("interaction projection requires id");
|
|
221
|
-
const key = this.interactionKey(scope, projection.id);
|
|
222
|
-
const prior = this.interactions.get(key);
|
|
223
|
-
const resolvedPrior = prior ?? (() => {
|
|
224
|
-
const alias = this.interactionAliases.get(key);
|
|
225
|
-
return alias ? this.interactions.get(alias) : void 0;
|
|
226
|
-
})();
|
|
227
|
-
if (!prior && resolvedPrior) {
|
|
228
|
-
this.interactionAliases.set(key, this.interactionKey(scope, resolvedPrior.id));
|
|
229
|
-
}
|
|
230
|
-
if (resolvedPrior) {
|
|
231
|
-
if (projection.eventId && resolvedPrior.eventId === projection.eventId) return resolvedPrior;
|
|
232
|
-
if (resolvedPrior.status !== "pending") {
|
|
233
|
-
if (resolvedPrior.tombstone && resolvedPrior.kind === "unknown" && projection.status === resolvedPrior.status) {
|
|
234
|
-
const enriched = {
|
|
235
|
-
...projection,
|
|
236
|
-
id: resolvedPrior.id,
|
|
237
|
-
status: resolvedPrior.status,
|
|
238
|
-
tombstone: true,
|
|
239
|
-
...resolvedPrior.cancelReason ? { cancelReason: resolvedPrior.cancelReason } : {}
|
|
240
|
-
};
|
|
241
|
-
this.interactions.set(this.interactionKey(scope, resolvedPrior.id), enriched);
|
|
242
|
-
return enriched;
|
|
243
|
-
}
|
|
244
|
-
if (resolvedPrior.status === projection.status && JSON.stringify(resolvedPrior.answers) === JSON.stringify(projection.answers)) return resolvedPrior;
|
|
245
|
-
throw new DurableChatConflictError("interaction already has a conflicting terminal outcome");
|
|
246
|
-
}
|
|
247
|
-
if (projection.status !== "pending") {
|
|
248
|
-
const canonical = resolvedPrior.id === projection.id ? projection : { ...projection, id: resolvedPrior.id };
|
|
249
|
-
this.interactions.set(this.interactionKey(scope, canonical.id), canonical);
|
|
250
|
-
if (resolvedPrior.semanticKey) {
|
|
251
|
-
this.interactionSemantic.delete(this.scopedKey(scope, resolvedPrior.semanticKey));
|
|
252
|
-
}
|
|
253
|
-
return canonical;
|
|
254
|
-
}
|
|
255
|
-
return resolvedPrior;
|
|
256
|
-
}
|
|
257
|
-
if (projection.semanticKey) {
|
|
258
|
-
const semantic = this.scopedKey(scope, projection.semanticKey);
|
|
259
|
-
const priorId = this.interactionSemantic.get(semantic);
|
|
260
|
-
if (priorId) {
|
|
261
|
-
const canonicalKey = this.interactionKey(scope, priorId);
|
|
262
|
-
this.interactionAliases.set(this.interactionKey(scope, projection.id), canonicalKey);
|
|
263
|
-
return this.interactions.get(canonicalKey) ?? projection;
|
|
264
|
-
}
|
|
265
|
-
this.interactionSemantic.set(semantic, projection.id);
|
|
266
|
-
}
|
|
267
|
-
this.interactions.set(key, projection);
|
|
268
|
-
return projection;
|
|
269
|
-
}
|
|
270
|
-
async getAnswerIntent(scope, intentKey2) {
|
|
271
|
-
return this.answerIntents.get(this.intentKey(scope, intentKey2)) ?? null;
|
|
272
|
-
}
|
|
273
|
-
async claimAnswerIntent(scope, intent) {
|
|
274
|
-
const key = this.intentKey(scope, intent.intentKey);
|
|
275
|
-
const existing = this.answerIntents.get(key);
|
|
276
|
-
if (!existing) {
|
|
277
|
-
this.answerIntents.set(key, intent);
|
|
278
|
-
return { status: "claimed", record: intent };
|
|
279
|
-
}
|
|
280
|
-
if (existing.interactionId === intent.interactionId && existing.attemptKey === intent.attemptKey && existing.outcome === intent.outcome && JSON.stringify(existing.data) === JSON.stringify(intent.data)) {
|
|
281
|
-
return { status: "existing", record: existing };
|
|
282
|
-
}
|
|
283
|
-
return { status: "conflict", record: existing, reason: "answer intent key is already used by another answer" };
|
|
284
|
-
}
|
|
285
|
-
async acknowledgeAnswerIntent(scope, intentKey2, acknowledgement) {
|
|
286
|
-
const intent = this.answerIntents.get(this.intentKey(scope, intentKey2));
|
|
287
|
-
if (!intent) throw new DurableChatConflictError("cannot acknowledge unknown answer intent");
|
|
288
|
-
if (intent.state === "finalized") return;
|
|
289
|
-
intent.acknowledgement = acknowledgement;
|
|
290
|
-
intent.state = "acknowledged";
|
|
291
|
-
}
|
|
292
|
-
async finalizeAnswerIntent(scope, intentKey2, guarantee = "reconciled") {
|
|
293
|
-
const intent = this.answerIntents.get(this.intentKey(scope, intentKey2));
|
|
294
|
-
if (!intent) throw new DurableChatConflictError("cannot finalize unknown answer intent");
|
|
295
|
-
if (intent.state === "finalized") return;
|
|
296
|
-
if (!intent.acknowledgement?.acknowledged) {
|
|
297
|
-
throw new DurableChatConflictError("cannot finalize an answer before authority acknowledgement");
|
|
298
|
-
}
|
|
299
|
-
const projection = await this.getInteractionProjection(scope, intent.interactionId);
|
|
300
|
-
if (!projection) throw new DurableChatConflictError("cannot finalize an answer before its ask projection");
|
|
301
|
-
const status = intent.outcome === "accepted" ? "answered" : "declined";
|
|
302
|
-
await this.upsertInteractionProjection(scope, {
|
|
303
|
-
...projection,
|
|
304
|
-
status,
|
|
305
|
-
...intent.data ? { answers: intent.data } : {},
|
|
306
|
-
updatedAt: intent.acknowledgement.at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
307
|
-
});
|
|
308
|
-
intent.state = "finalized";
|
|
309
|
-
intent.guarantee = guarantee;
|
|
310
|
-
intent.finalizedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
311
|
-
}
|
|
312
|
-
async abortAnswerIntent(scope, intentKey2, error) {
|
|
313
|
-
const intent = this.answerIntents.get(this.intentKey(scope, intentKey2));
|
|
314
|
-
if (!intent) throw new DurableChatConflictError("cannot abort unknown answer intent");
|
|
315
|
-
if (intent.state === "finalized") return;
|
|
316
|
-
intent.state = "aborted";
|
|
317
|
-
intent.error = error;
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
var InMemoryDurableChatStore = InMemoryDurableChatStateStore;
|
|
321
|
-
function createInMemoryDurableChatStateStore() {
|
|
322
|
-
return new InMemoryDurableChatStateStore();
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// src/durable-chat/interactions.ts
|
|
326
|
-
function nowIso(now) {
|
|
327
|
-
return (now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
|
|
328
|
-
}
|
|
329
|
-
function intentKey(scope, interactionId, attemptKey) {
|
|
330
|
-
return `interaction-answer:${encodeURIComponent(scope)}:${encodeURIComponent(interactionId)}:${encodeURIComponent(attemptKey)}`;
|
|
331
|
-
}
|
|
332
|
-
function ensureAttemptKey(attemptKey) {
|
|
333
|
-
if (!attemptKey || typeof attemptKey !== "string" || attemptKey.length > 512) throw new TypeError("attemptKey must be a non-empty string");
|
|
334
|
-
}
|
|
335
|
-
function safeAnswers(data) {
|
|
336
|
-
if (data === void 0) return void 0;
|
|
337
|
-
for (const key of Object.keys(data)) {
|
|
338
|
-
if (!isSafeInteractionFieldKey(key)) throw new TypeError(`unsafe interaction answer key: ${key}`);
|
|
339
|
-
}
|
|
340
|
-
const parsed = parseInteractionAnswers(data);
|
|
341
|
-
if (!parsed.succeeded) throw new TypeError(parsed.error);
|
|
342
|
-
return parsed.value;
|
|
343
|
-
}
|
|
344
|
-
async function upsertDurableInteractionAsk(store, scope, request, options = {}) {
|
|
345
|
-
const semanticKey = options.semanticKey ?? questionInteractionContentSignature(interactionFromWireRequest(request)) ?? void 0;
|
|
346
|
-
const incoming = {
|
|
347
|
-
...interactionFromWireRequest(request),
|
|
348
|
-
...options.eventId ? { eventId: options.eventId } : {},
|
|
349
|
-
...semanticKey ? { semanticKey } : {},
|
|
350
|
-
updatedAt: nowIso(options.now)
|
|
351
|
-
};
|
|
352
|
-
const prior = await store.getInteractionProjection(scope, request.id);
|
|
353
|
-
if (prior?.tombstone) {
|
|
354
|
-
return store.upsertInteractionProjection(scope, {
|
|
355
|
-
...incoming,
|
|
356
|
-
status: prior.status,
|
|
357
|
-
tombstone: true,
|
|
358
|
-
...prior.cancelReason ? { cancelReason: prior.cancelReason } : {}
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
if (prior && prior.status !== "pending") return prior;
|
|
362
|
-
return store.upsertInteractionProjection(scope, incoming);
|
|
363
|
-
}
|
|
364
|
-
async function recordDurableInteractionCancel(store, scope, interactionId, reason, options = {}) {
|
|
365
|
-
if (!interactionId) throw new TypeError("interaction id is required");
|
|
366
|
-
const status = cancelStatusFor(reason);
|
|
367
|
-
const prior = await store.getInteractionProjection(scope, interactionId);
|
|
368
|
-
if (prior) {
|
|
369
|
-
if (prior.status === status) return prior;
|
|
370
|
-
if (prior.status !== "pending") throw new DurableChatConflictError("interaction already has a conflicting terminal outcome");
|
|
371
|
-
return store.upsertInteractionProjection(scope, {
|
|
372
|
-
...prior,
|
|
373
|
-
status,
|
|
374
|
-
tombstone: true,
|
|
375
|
-
...reason ? { cancelReason: reason } : {},
|
|
376
|
-
...options.eventId ? { eventId: options.eventId } : {},
|
|
377
|
-
updatedAt: nowIso(options.now)
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
return store.upsertInteractionProjection(scope, {
|
|
381
|
-
id: interactionId,
|
|
382
|
-
kind: "unknown",
|
|
383
|
-
title: "",
|
|
384
|
-
fields: [],
|
|
385
|
-
status,
|
|
386
|
-
tombstone: true,
|
|
387
|
-
...reason ? { cancelReason: reason } : {},
|
|
388
|
-
...options.eventId ? { eventId: options.eventId } : {},
|
|
389
|
-
updatedAt: nowIso(options.now)
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
async function recordDurableInteractionAnswer(store, scope, interactionId, outcome, answers, options = {}) {
|
|
393
|
-
const prior = await store.getInteractionProjection(scope, interactionId);
|
|
394
|
-
if (!prior) throw new DurableChatConflictError("cannot answer an interaction before its ask projection");
|
|
395
|
-
const status = outcome === "accepted" ? "answered" : "declined";
|
|
396
|
-
const parsed = safeAnswers(answers);
|
|
397
|
-
if (prior.status === status && JSON.stringify(prior.answers) === JSON.stringify(parsed)) return prior;
|
|
398
|
-
if (prior.status !== "pending") throw new DurableChatConflictError("interaction already has a conflicting terminal outcome");
|
|
399
|
-
return store.upsertInteractionProjection(scope, {
|
|
400
|
-
...prior,
|
|
401
|
-
status,
|
|
402
|
-
...parsed ? { answers: parsed } : {},
|
|
403
|
-
...options.eventId ? { eventId: options.eventId } : {},
|
|
404
|
-
updatedAt: nowIso(options.now)
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
function createDurableInteractionSettlement(options) {
|
|
408
|
-
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
409
|
-
return {
|
|
410
|
-
async prepare(scope, interactionId, outcome, data) {
|
|
411
|
-
ensureAttemptKey(options.attemptKey);
|
|
412
|
-
const parsed = safeAnswers(data);
|
|
413
|
-
const record = {
|
|
414
|
-
scope,
|
|
415
|
-
interactionId,
|
|
416
|
-
attemptKey: options.attemptKey,
|
|
417
|
-
intentKey: intentKey(scope, interactionId, options.attemptKey),
|
|
418
|
-
outcome,
|
|
419
|
-
...parsed ? { data: parsed } : {},
|
|
420
|
-
state: "prepared",
|
|
421
|
-
guarantee: options.guarantee ?? "reconciled",
|
|
422
|
-
createdAt: now()
|
|
423
|
-
};
|
|
424
|
-
const result = await options.store.claimAnswerIntent(scope, record);
|
|
425
|
-
if (result.status === "conflict") throw new DurableChatConflictError(result.reason);
|
|
426
|
-
return result.record;
|
|
427
|
-
},
|
|
428
|
-
async acknowledge(scope, key, acknowledgement = {}) {
|
|
429
|
-
const existing = await options.store.getAnswerIntent(scope, key);
|
|
430
|
-
if (!existing) throw new DurableChatConflictError("unknown answer intent");
|
|
431
|
-
const value = {
|
|
432
|
-
acknowledged: true,
|
|
433
|
-
...acknowledgement.authorityId ? { authorityId: acknowledgement.authorityId } : {},
|
|
434
|
-
...acknowledgement.status ? { status: acknowledgement.status } : {},
|
|
435
|
-
at: acknowledgement.at ?? now()
|
|
436
|
-
};
|
|
437
|
-
await options.store.acknowledgeAnswerIntent(scope, key, value);
|
|
438
|
-
return await options.store.getAnswerIntent(scope, key);
|
|
439
|
-
},
|
|
440
|
-
async finalize(scope, key) {
|
|
441
|
-
const existing = await options.store.getAnswerIntent(scope, key);
|
|
442
|
-
if (!existing) throw new DurableChatConflictError("unknown answer intent");
|
|
443
|
-
await options.store.finalizeAnswerIntent(scope, key, options.guarantee ?? "reconciled");
|
|
444
|
-
return await options.store.getAnswerIntent(scope, key);
|
|
445
|
-
},
|
|
446
|
-
async abort(scope, key, error) {
|
|
447
|
-
const existing = await options.store.getAnswerIntent(scope, key);
|
|
448
|
-
if (!existing) throw new DurableChatConflictError("unknown answer intent");
|
|
449
|
-
await options.store.abortAnswerIntent(scope, key, error);
|
|
450
|
-
return await options.store.getAnswerIntent(scope, key);
|
|
451
|
-
},
|
|
452
|
-
async reconcile(scope, key) {
|
|
453
|
-
const existing = await options.store.getAnswerIntent(scope, key);
|
|
454
|
-
if (!existing || !options.reconcileAuthority) return existing;
|
|
455
|
-
if (existing.state === "finalized") return existing;
|
|
456
|
-
const acknowledgement = await options.reconcileAuthority({ scope, intent: existing });
|
|
457
|
-
if (!acknowledgement) return existing;
|
|
458
|
-
await options.store.acknowledgeAnswerIntent(scope, key, {
|
|
459
|
-
acknowledged: true,
|
|
460
|
-
...acknowledgement.authorityId ? { authorityId: acknowledgement.authorityId } : {},
|
|
461
|
-
...acknowledgement.status ? { status: acknowledgement.status } : {},
|
|
462
|
-
...acknowledgement.at ? { at: acknowledgement.at } : {}
|
|
463
|
-
});
|
|
464
|
-
await options.store.finalizeAnswerIntent(scope, key, options.guarantee ?? "reconciled");
|
|
465
|
-
return await options.store.getAnswerIntent(scope, key);
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
function durableInteractionIntentKey(scope, interactionId, attemptKey) {
|
|
470
|
-
ensureAttemptKey(attemptKey);
|
|
471
|
-
return intentKey(scope, interactionId, attemptKey);
|
|
472
|
-
}
|
|
473
|
-
var applyDurableInteractionAsk = upsertDurableInteractionAsk;
|
|
474
|
-
var applyDurableInteractionCancel = recordDurableInteractionCancel;
|
|
475
|
-
var applyDurableInteractionAnswer = recordDurableInteractionAnswer;
|
|
476
|
-
|
|
477
|
-
// src/durable-chat/adapters.ts
|
|
478
|
-
function createDurableInteractionProjectionAdapter(options) {
|
|
479
|
-
const observed = /* @__PURE__ */ new Set();
|
|
480
|
-
return {
|
|
481
|
-
async upsertAsk(request) {
|
|
482
|
-
observed.add(request.id);
|
|
483
|
-
await upsertDurableInteractionAsk(options.store, options.scope, request, { now: options.now });
|
|
484
|
-
},
|
|
485
|
-
async cancel(cancel) {
|
|
486
|
-
observed.add(cancel.id);
|
|
487
|
-
await recordDurableInteractionCancel(
|
|
488
|
-
options.store,
|
|
489
|
-
options.scope,
|
|
490
|
-
cancel.id,
|
|
491
|
-
cancel.reason,
|
|
492
|
-
{ now: options.now }
|
|
493
|
-
);
|
|
494
|
-
},
|
|
495
|
-
async materialize() {
|
|
496
|
-
const projections = await Promise.all(
|
|
497
|
-
[...observed].map((id) => options.store.getInteractionProjection(options.scope, id))
|
|
498
|
-
);
|
|
499
|
-
const canonical = new Map(projections.filter((item) => item !== null).map((item) => [item.id, item]));
|
|
500
|
-
return [...canonical.values()].filter((projection) => projection.kind !== "unknown").map((projection) => interactionToPersistedPart(
|
|
501
|
-
{
|
|
502
|
-
id: projection.id,
|
|
503
|
-
kind: projection.kind,
|
|
504
|
-
title: projection.title,
|
|
505
|
-
...projection.body ? { body: projection.body } : {},
|
|
506
|
-
answerSpec: { fields: projection.fields }
|
|
507
|
-
},
|
|
508
|
-
projection.status,
|
|
509
|
-
projection.cancelReason,
|
|
510
|
-
projection.answers
|
|
511
|
-
));
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
function eventRecord(value) {
|
|
516
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
517
|
-
const record = value;
|
|
518
|
-
if (record.kind === "event" && record.event && typeof record.event === "object") {
|
|
519
|
-
return record.event;
|
|
520
|
-
}
|
|
521
|
-
return record;
|
|
522
|
-
}
|
|
523
|
-
function createDurableChatEventProjection(options) {
|
|
524
|
-
const interactions = createDurableInteractionProjectionAdapter(options);
|
|
525
|
-
const plans = /* @__PURE__ */ new Set();
|
|
526
|
-
return {
|
|
527
|
-
async observe(value) {
|
|
528
|
-
const event = eventRecord(value);
|
|
529
|
-
if (!event || typeof event.type !== "string") return;
|
|
530
|
-
const data = eventRecord(event.data);
|
|
531
|
-
if (event.type === "interaction") {
|
|
532
|
-
const parsed = parseInteractionRequest(data ?? void 0);
|
|
533
|
-
if (parsed.succeeded) await interactions.upsertAsk(parsed.value);
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
if (event.type === "interaction.cancel") {
|
|
537
|
-
const parsed = parseInteractionCancel(data ?? void 0);
|
|
538
|
-
if (parsed.succeeded) {
|
|
539
|
-
try {
|
|
540
|
-
await interactions.cancel(parsed.value);
|
|
541
|
-
} catch (error) {
|
|
542
|
-
if (!(error instanceof DurableChatConflictError)) throw error;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if (!event.type.startsWith("plan.")) return;
|
|
548
|
-
const submitted = parsePlanSubmittedEvent(event);
|
|
549
|
-
const planRecord = eventRecord(data?.plan) ?? eventRecord(eventRecord(event.properties)?.plan);
|
|
550
|
-
const plan = submitted.succeeded ? submitted.value : planRecord ? persistedPartToPlan({ type: "plan", ...planRecord }) : null;
|
|
551
|
-
if (!plan) return;
|
|
552
|
-
plans.add(`${plan.planId}\0${plan.revision}`);
|
|
553
|
-
try {
|
|
554
|
-
await options.store.putPlanProjection(options.scope, plan);
|
|
555
|
-
} catch (error) {
|
|
556
|
-
if (!(error instanceof DurableChatConflictError)) throw error;
|
|
557
|
-
}
|
|
558
|
-
},
|
|
559
|
-
async materialize() {
|
|
560
|
-
const interactionParts = await interactions.materialize();
|
|
561
|
-
const planParts = await Promise.all([...plans].map(async (key) => {
|
|
562
|
-
const [planId, rawRevision] = key.split("\0");
|
|
563
|
-
const plan = await options.store.getPlanProjection(options.scope, planId, Number(rawRevision));
|
|
564
|
-
return plan ? planToPersistedPart(plan) : null;
|
|
565
|
-
}));
|
|
566
|
-
return [...interactionParts, ...planParts.filter((part) => part !== null)];
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
}
|
|
570
|
-
function createDurableInteractionRoutePersistence(options) {
|
|
571
|
-
const guarantee = options.guarantee;
|
|
572
|
-
return {
|
|
573
|
-
guarantee,
|
|
574
|
-
async prepare(args) {
|
|
575
|
-
const scope = await options.scope(args);
|
|
576
|
-
const settlement = createDurableInteractionSettlement({
|
|
577
|
-
store: options.store,
|
|
578
|
-
attemptKey: args.attemptKey,
|
|
579
|
-
guarantee,
|
|
580
|
-
now: options.now,
|
|
581
|
-
...options.reconcileAuthority ? { reconcileAuthority: ({ intent: intent2 }) => options.reconcileAuthority({ scope, intent: intent2, route: args }) } : {}
|
|
582
|
-
});
|
|
583
|
-
const intent = await settlement.prepare(
|
|
584
|
-
scope,
|
|
585
|
-
args.answer.id,
|
|
586
|
-
args.answer.outcome,
|
|
587
|
-
args.answer.data
|
|
588
|
-
);
|
|
589
|
-
return { scope, settlement, intent };
|
|
590
|
-
},
|
|
591
|
-
async reconcile({ prepared }) {
|
|
592
|
-
let intent = await prepared.settlement.reconcile(prepared.scope, prepared.intent.intentKey);
|
|
593
|
-
if (intent?.state === "acknowledged") {
|
|
594
|
-
intent = await prepared.settlement.finalize(prepared.scope, prepared.intent.intentKey);
|
|
595
|
-
}
|
|
596
|
-
return { settled: intent?.state === "finalized" };
|
|
597
|
-
},
|
|
598
|
-
async acknowledge({ prepared }) {
|
|
599
|
-
await prepared.settlement.acknowledge(prepared.scope, prepared.intent.intentKey, {
|
|
600
|
-
status: prepared.intent.outcome
|
|
601
|
-
});
|
|
602
|
-
},
|
|
603
|
-
async finalize({ prepared }) {
|
|
604
|
-
await prepared.settlement.finalize(prepared.scope, prepared.intent.intentKey);
|
|
605
|
-
},
|
|
606
|
-
async fail({ prepared, error }) {
|
|
607
|
-
await prepared.settlement.abort(
|
|
608
|
-
prepared.scope,
|
|
609
|
-
prepared.intent.intentKey,
|
|
610
|
-
error instanceof Error ? error.message : String(error)
|
|
611
|
-
);
|
|
612
|
-
}
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
// src/durable-chat/plan-routes.ts
|
|
617
|
-
function errorResponse(error, fallbackCode, fallbackStatus, fallbackMessage) {
|
|
618
|
-
if (error instanceof Response) return error;
|
|
619
|
-
if (error && typeof error === "object" && "status" in error && typeof error.status === "number") {
|
|
620
|
-
const typed = error;
|
|
621
|
-
return Response.json(
|
|
622
|
-
{ code: typeof typed.code === "string" ? typed.code : fallbackCode, error: typeof typed.message === "string" ? typed.message : fallbackMessage },
|
|
623
|
-
{ status: typed.status }
|
|
624
|
-
);
|
|
625
|
-
}
|
|
626
|
-
return Response.json({ code: fallbackCode, error: fallbackMessage }, { status: fallbackStatus });
|
|
627
|
-
}
|
|
628
|
-
function scopeFromAuthorization(value) {
|
|
629
|
-
if (value instanceof Response) return value;
|
|
630
|
-
if (typeof value === "string" && value) return value;
|
|
631
|
-
if (value && typeof value === "object" && "scope" in value && value.scope) return value.scope;
|
|
632
|
-
return Response.json({ code: "DURABLE_CHAT_UNAUTHORIZED", error: "not authorized" }, { status: 401 });
|
|
633
|
-
}
|
|
634
|
-
function planIdFromUrl(request) {
|
|
635
|
-
const url = new URL(request.url);
|
|
636
|
-
const query = url.searchParams.get("planId") ?? url.searchParams.get("id");
|
|
637
|
-
if (query) return query;
|
|
638
|
-
const segments = url.pathname.split("/").filter(Boolean);
|
|
639
|
-
return segments.length > 0 ? decodeURIComponent(segments[segments.length - 1]) : null;
|
|
640
|
-
}
|
|
641
|
-
function asCurrent(value) {
|
|
642
|
-
if (value === null) return { plan: null };
|
|
643
|
-
if ("plan" in value) return value;
|
|
644
|
-
return { plan: value };
|
|
645
|
-
}
|
|
646
|
-
function terminalDecision(plan) {
|
|
647
|
-
if (!plan) return null;
|
|
648
|
-
return plan.status === "approved" || plan.status === "rejected" ? plan.status : null;
|
|
649
|
-
}
|
|
650
|
-
function parseDecisionBody(body) {
|
|
651
|
-
if (!body || typeof body !== "object" || Array.isArray(body)) return { error: "Invalid JSON body" };
|
|
652
|
-
const record = body;
|
|
653
|
-
const planId = typeof record.planId === "string" && record.planId ? record.planId : typeof record.id === "string" && record.id ? record.id : null;
|
|
654
|
-
const revision = record.revision;
|
|
655
|
-
const decision = normalizePlanDecision(record.decision ?? record.outcome ?? record.status);
|
|
656
|
-
if (!planId) return { error: "Missing planId" };
|
|
657
|
-
if (typeof revision !== "number" || !Number.isInteger(revision) || revision < 1) return { error: "revision must be a positive integer" };
|
|
658
|
-
if (!decision) return { error: "decision must be approved or rejected" };
|
|
659
|
-
if (record.feedback !== void 0 && typeof record.feedback !== "string") return { error: "feedback must be a string" };
|
|
660
|
-
return { planId, revision, decision, ...typeof record.feedback === "string" ? { feedback: record.feedback } : {} };
|
|
661
|
-
}
|
|
662
|
-
function withReceipt(result, scope, parsed) {
|
|
663
|
-
const receipt = result.receipt ?? stablePlanReceipt(scope, parsed.planId, parsed.revision, parsed.decision, result);
|
|
664
|
-
return { ...result, receipt };
|
|
665
|
-
}
|
|
666
|
-
function recoverReceipt(scope, plan, decision) {
|
|
667
|
-
return stablePlanReceipt(scope, plan.planId, plan.revision, decision, {
|
|
668
|
-
followUp: { turnId: planFollowUpTurnId(plan.planId, decision), state: "unknown" }
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
function createDurablePlanRoutes(options) {
|
|
672
|
-
const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
673
|
-
const logger = options.logger ?? console;
|
|
674
|
-
async function authorize(request, operation, planId) {
|
|
675
|
-
try {
|
|
676
|
-
return scopeFromAuthorization(await options.authorize({ request, operation, ...planId ? { planId } : {} }));
|
|
677
|
-
} catch (error) {
|
|
678
|
-
return errorResponse(error, "DURABLE_CHAT_UNAUTHORIZED", 401, "not authorized");
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
async function runEffect(scope, plan, receipt) {
|
|
682
|
-
const effectKey = planEffectKey(scope, plan.planId, plan.revision, receipt.decision);
|
|
683
|
-
try {
|
|
684
|
-
const existing = await options.store.getPlanEffect(scope, effectKey);
|
|
685
|
-
if (existing?.state === "completed") return true;
|
|
686
|
-
const claim = await options.store.claimPlanEffect(scope, {
|
|
687
|
-
effectKey,
|
|
688
|
-
scope,
|
|
689
|
-
planId: plan.planId,
|
|
690
|
-
revision: plan.revision,
|
|
691
|
-
decision: receipt.decision,
|
|
692
|
-
state: "claimed",
|
|
693
|
-
claimedAt: now()
|
|
694
|
-
});
|
|
695
|
-
if (claim.status === "existing" && claim.record.state === "completed") return true;
|
|
696
|
-
await options.afterDecision({ scope, plan, receipt, effectKey });
|
|
697
|
-
await options.store.completePlanEffect(scope, effectKey);
|
|
698
|
-
return true;
|
|
699
|
-
} catch (error) {
|
|
700
|
-
await options.store.failPlanEffect(scope, effectKey, error instanceof Error ? error.message : String(error)).catch(() => void 0);
|
|
701
|
-
logger.warn("[durable-chat] afterDecision failed:", error);
|
|
702
|
-
return false;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
async function current(request) {
|
|
706
|
-
const planId = planIdFromUrl(request);
|
|
707
|
-
if (!planId) return Response.json({ code: "DURABLE_CHAT_BAD_REQUEST", error: "Missing planId" }, { status: 400 });
|
|
708
|
-
const scope = await authorize(request, "current", planId);
|
|
709
|
-
if (scope instanceof Response) return scope;
|
|
710
|
-
const local = await options.store.getPlanProjection(scope, planId);
|
|
711
|
-
let authoritative;
|
|
712
|
-
try {
|
|
713
|
-
authoritative = asCurrent(await options.authority.current({ scope, planId }));
|
|
714
|
-
} catch (error) {
|
|
715
|
-
const decision2 = terminalDecision(local);
|
|
716
|
-
if (decision2) {
|
|
717
|
-
const command = await options.store.getPlanCommand(scope, planCommandKey(planId, local.revision, decision2));
|
|
718
|
-
if (command?.receipt) {
|
|
719
|
-
const effectComplete2 = await runEffect(scope, command.authorityResult?.plan ?? local, command.receipt);
|
|
720
|
-
return Response.json({
|
|
721
|
-
plan: command.authorityResult?.plan ?? local,
|
|
722
|
-
receipt: command.receipt,
|
|
723
|
-
replayed: true,
|
|
724
|
-
...!effectComplete2 ? { effectPending: true } : {}
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
return errorResponse(error, "DURABLE_CHAT_UNAVAILABLE", 503, "plan authority unavailable");
|
|
729
|
-
}
|
|
730
|
-
if (!authoritative.plan) {
|
|
731
|
-
return Response.json({ code: "DURABLE_CHAT_GONE", error: "plan is no longer available" }, { status: 410 });
|
|
732
|
-
}
|
|
733
|
-
const authorityPlan = authoritative.plan;
|
|
734
|
-
const localWins = Boolean(local && (local.revision > authorityPlan.revision || local.revision === authorityPlan.revision && !canTransitionPlanStatus(local.status, authorityPlan.status)));
|
|
735
|
-
const plan = localWins ? local : authorityPlan;
|
|
736
|
-
let projectionPending = false;
|
|
737
|
-
if (!localWins) {
|
|
738
|
-
try {
|
|
739
|
-
await options.store.putPlanProjection(scope, plan);
|
|
740
|
-
} catch (error) {
|
|
741
|
-
projectionPending = true;
|
|
742
|
-
logger.warn("[durable-chat] failed to heal plan projection:", error);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
const decision = terminalDecision(plan);
|
|
746
|
-
let receipt = localWins ? void 0 : authoritative.receipt;
|
|
747
|
-
if (!receipt && decision) {
|
|
748
|
-
const command = await options.store.getPlanCommand(scope, planCommandKey(plan.planId, plan.revision, decision));
|
|
749
|
-
receipt = command?.receipt;
|
|
750
|
-
if (!receipt) receipt = recoverReceipt(scope, plan, decision);
|
|
751
|
-
}
|
|
752
|
-
const effectComplete = decision && receipt ? await runEffect(scope, plan, receipt) : true;
|
|
753
|
-
return Response.json({
|
|
754
|
-
plan,
|
|
755
|
-
...receipt ? { receipt } : {},
|
|
756
|
-
...projectionPending ? { projectionPending: true } : {},
|
|
757
|
-
...!effectComplete ? { effectPending: true } : {}
|
|
758
|
-
});
|
|
759
|
-
}
|
|
760
|
-
async function decide(request) {
|
|
761
|
-
const body = await request.json().catch(() => null);
|
|
762
|
-
const parsed = parseDecisionBody(body);
|
|
763
|
-
if ("error" in parsed) return Response.json({ code: "DURABLE_CHAT_BAD_REQUEST", error: parsed.error }, { status: 400 });
|
|
764
|
-
const scope = await authorize(request, "decide", parsed.planId);
|
|
765
|
-
if (scope instanceof Response) return scope;
|
|
766
|
-
const local = await options.store.getPlanProjection(scope, parsed.planId, parsed.revision);
|
|
767
|
-
const current2 = await options.store.getPlanProjection(scope, parsed.planId);
|
|
768
|
-
if (!local) {
|
|
769
|
-
return Response.json({ code: "DURABLE_CHAT_UNAVAILABLE", error: "submitted plan projection is required before deciding" }, { status: 503 });
|
|
770
|
-
}
|
|
771
|
-
if (current2 && parsed.revision < current2.revision) {
|
|
772
|
-
return Response.json({ code: "DURABLE_CHAT_CONFLICT", error: "stale plan revision" }, { status: 409 });
|
|
773
|
-
}
|
|
774
|
-
if (local.status !== "pending") {
|
|
775
|
-
const existingTerminal = terminalDecision(local);
|
|
776
|
-
if (existingTerminal === parsed.decision) {
|
|
777
|
-
const existing = await options.store.getPlanCommand(scope, planCommandKey(parsed.planId, parsed.revision, parsed.decision));
|
|
778
|
-
const receipt = existing?.receipt ?? recoverReceipt(scope, local, parsed.decision);
|
|
779
|
-
const effectComplete2 = await runEffect(scope, existing?.authorityResult?.plan ?? local, receipt);
|
|
780
|
-
return Response.json({
|
|
781
|
-
plan: existing?.authorityResult?.plan ?? local,
|
|
782
|
-
receipt,
|
|
783
|
-
replayed: true,
|
|
784
|
-
...!effectComplete2 ? { effectPending: true } : {}
|
|
785
|
-
});
|
|
786
|
-
}
|
|
787
|
-
return Response.json({
|
|
788
|
-
code: "DURABLE_CHAT_CONFLICT",
|
|
789
|
-
error: "plan is not pending",
|
|
790
|
-
plan: local
|
|
791
|
-
}, { status: 409 });
|
|
792
|
-
}
|
|
793
|
-
const opposite = parsed.decision === "approved" ? "rejected" : "approved";
|
|
794
|
-
const oppositeCommand = await options.store.getPlanCommand(scope, planCommandKey(parsed.planId, parsed.revision, opposite));
|
|
795
|
-
if (oppositeCommand) return Response.json({ code: "DURABLE_CHAT_CONFLICT", error: "competing decision for plan revision" }, { status: 409 });
|
|
796
|
-
const commandKey = planCommandKey(parsed.planId, parsed.revision, parsed.decision);
|
|
797
|
-
const authorityIdempotencyKey = planAuthorityIdempotencyKey(scope, parsed.planId, parsed.revision, parsed.decision);
|
|
798
|
-
const command = {
|
|
799
|
-
scope,
|
|
800
|
-
planId: parsed.planId,
|
|
801
|
-
revision: parsed.revision,
|
|
802
|
-
decision: parsed.decision,
|
|
803
|
-
commandKey,
|
|
804
|
-
authorityIdempotencyKey,
|
|
805
|
-
state: "claimed",
|
|
806
|
-
claimedAt: now()
|
|
807
|
-
};
|
|
808
|
-
const claimed = await options.store.claimPlanCommand(scope, command);
|
|
809
|
-
if (claimed.status === "conflict") return Response.json({ code: "DURABLE_CHAT_CONFLICT", error: claimed.reason }, { status: 409 });
|
|
810
|
-
if (claimed.status === "existing" && claimed.record.receipt) {
|
|
811
|
-
const effectComplete2 = await runEffect(scope, claimed.record.authorityResult?.plan ?? local, claimed.record.receipt);
|
|
812
|
-
return Response.json({
|
|
813
|
-
plan: claimed.record.authorityResult?.plan ?? local,
|
|
814
|
-
receipt: claimed.record.receipt,
|
|
815
|
-
replayed: true,
|
|
816
|
-
...!effectComplete2 ? { effectPending: true } : {}
|
|
817
|
-
});
|
|
818
|
-
}
|
|
819
|
-
let authorityResult;
|
|
820
|
-
try {
|
|
821
|
-
authorityResult = withReceipt(await options.authority.decide({
|
|
822
|
-
scope,
|
|
823
|
-
planId: parsed.planId,
|
|
824
|
-
revision: parsed.revision,
|
|
825
|
-
decision: parsed.decision,
|
|
826
|
-
...parsed.feedback ? { feedback: parsed.feedback } : {},
|
|
827
|
-
idempotencyKey: authorityIdempotencyKey
|
|
828
|
-
}), scope, parsed);
|
|
829
|
-
} catch (error) {
|
|
830
|
-
try {
|
|
831
|
-
const reconciled = asCurrent(await options.authority.current({
|
|
832
|
-
scope,
|
|
833
|
-
planId: parsed.planId,
|
|
834
|
-
revision: parsed.revision
|
|
835
|
-
}));
|
|
836
|
-
const terminal = terminalDecision(reconciled.plan);
|
|
837
|
-
if (reconciled.plan && terminal === parsed.decision) {
|
|
838
|
-
const receipt = reconciled.receipt ?? recoverReceipt(scope, reconciled.plan, terminal);
|
|
839
|
-
authorityResult = {
|
|
840
|
-
plan: reconciled.plan,
|
|
841
|
-
followUp: { turnId: receipt.turnId, state: receipt.state },
|
|
842
|
-
idempotent: true,
|
|
843
|
-
receipt
|
|
844
|
-
};
|
|
845
|
-
} else if (reconciled.plan && reconciled.plan.status !== "pending") {
|
|
846
|
-
let projectionPending2 = false;
|
|
847
|
-
try {
|
|
848
|
-
await options.store.putPlanProjection(scope, reconciled.plan);
|
|
849
|
-
} catch (projectionError) {
|
|
850
|
-
projectionPending2 = true;
|
|
851
|
-
logger.warn("[durable-chat] failed to reconcile conflicting plan projection:", projectionError);
|
|
852
|
-
}
|
|
853
|
-
return Response.json({
|
|
854
|
-
code: "DURABLE_CHAT_CONFLICT",
|
|
855
|
-
error: "plan is not pending",
|
|
856
|
-
plan: reconciled.plan,
|
|
857
|
-
...reconciled.receipt ? { receipt: reconciled.receipt } : {},
|
|
858
|
-
...projectionPending2 ? { projectionPending: true } : {}
|
|
859
|
-
}, { status: 409 });
|
|
860
|
-
} else {
|
|
861
|
-
return errorResponse(error, "DURABLE_CHAT_UNAVAILABLE", 503, "plan authority unavailable");
|
|
862
|
-
}
|
|
863
|
-
} catch {
|
|
864
|
-
return errorResponse(error, "DURABLE_CHAT_UNAVAILABLE", 503, "plan authority unavailable");
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
let projectionPending = false;
|
|
868
|
-
try {
|
|
869
|
-
await options.store.recordPlanAuthorityResult(scope, commandKey, authorityResult, authorityResult.receipt);
|
|
870
|
-
} catch (error) {
|
|
871
|
-
projectionPending = true;
|
|
872
|
-
logger.warn("[durable-chat] failed to record authority result:", error);
|
|
873
|
-
}
|
|
874
|
-
try {
|
|
875
|
-
await options.store.putPlanProjection(scope, authorityResult.plan);
|
|
876
|
-
await options.store.finalizePlanCommand(scope, commandKey);
|
|
877
|
-
} catch (error) {
|
|
878
|
-
projectionPending = true;
|
|
879
|
-
logger.warn("[durable-chat] failed to finalize plan projection:", error);
|
|
880
|
-
}
|
|
881
|
-
const effectComplete = await runEffect(scope, authorityResult.plan, authorityResult.receipt);
|
|
882
|
-
return Response.json({
|
|
883
|
-
plan: authorityResult.plan,
|
|
884
|
-
receipt: authorityResult.receipt,
|
|
885
|
-
...projectionPending ? { projectionPending: true } : {},
|
|
886
|
-
...!effectComplete ? { effectPending: true } : {}
|
|
887
|
-
});
|
|
888
|
-
}
|
|
889
|
-
return { current, decide };
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
export {
|
|
893
|
-
DurableChatError,
|
|
894
|
-
DurableChatConflictError,
|
|
895
|
-
DurableChatUnavailableError,
|
|
896
|
-
DurableChatGoneError,
|
|
897
|
-
createDurableChatScope,
|
|
898
|
-
durableChatScopeKey,
|
|
899
|
-
normalizePlanDecision,
|
|
900
|
-
planCommandKey,
|
|
901
|
-
planAuthorityIdempotencyKey,
|
|
902
|
-
planEffectKey,
|
|
903
|
-
stablePlanReceipt,
|
|
904
|
-
InMemoryDurableChatStateStore,
|
|
905
|
-
InMemoryDurableChatStore,
|
|
906
|
-
createInMemoryDurableChatStateStore,
|
|
907
|
-
upsertDurableInteractionAsk,
|
|
908
|
-
recordDurableInteractionCancel,
|
|
909
|
-
recordDurableInteractionAnswer,
|
|
910
|
-
createDurableInteractionSettlement,
|
|
911
|
-
durableInteractionIntentKey,
|
|
912
|
-
applyDurableInteractionAsk,
|
|
913
|
-
applyDurableInteractionCancel,
|
|
914
|
-
applyDurableInteractionAnswer,
|
|
915
|
-
createDurableInteractionProjectionAdapter,
|
|
916
|
-
createDurableChatEventProjection,
|
|
917
|
-
createDurableInteractionRoutePersistence,
|
|
918
|
-
createDurablePlanRoutes
|
|
919
|
-
};
|
|
920
|
-
//# sourceMappingURL=chunk-6GQ5DKFK.js.map
|