effect-agent 0.1.0-beta.140 → 0.1.0-beta.142
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/{AgentRuntime-RdmnK_aD.mjs → AgentRuntime-xeCUXutN.mjs} +10 -9
- package/dist/AgentRuntime-xeCUXutN.mjs.map +1 -0
- package/dist/{DurableAgentRuntime-BmG_UVZG.mjs → DurableAgentRuntime-DQXDcPKQ.mjs} +2 -2
- package/dist/{DurableAgentRuntime-BmG_UVZG.mjs.map → DurableAgentRuntime-DQXDcPKQ.mjs.map} +1 -1
- package/dist/{Subagent-Bnc7I-kb.mjs → Subagent-8RzJkqJ9.mjs} +2 -2
- package/dist/{Subagent-Bnc7I-kb.mjs.map → Subagent-8RzJkqJ9.mjs.map} +1 -1
- package/dist/capabilities/Subagent.mjs +1 -1
- package/dist/durable/DurableAgentRuntime.mjs +1 -1
- package/dist/engine/AgentRuntime.mjs +1 -1
- package/dist/index.d.mts +1 -2
- package/dist/index.mjs +4 -5
- package/package.json +1 -1
- package/src/engine/internal/agent-runtime.ts +49 -19
- package/src/index.ts +0 -2
- package/dist/AgentRuntime-RdmnK_aD.mjs.map +0 -1
- package/dist/durable/SqlMessageDeliveryStore.d.mts +0 -42
- package/dist/durable/SqlMessageDeliveryStore.mjs +0 -179
- package/dist/durable/SqlMessageDeliveryStore.mjs.map +0 -1
- package/dist/durable/SqlStorageV2Upgrade.d.mts +0 -21
- package/dist/durable/SqlStorageV2Upgrade.mjs +0 -459
- package/dist/durable/SqlStorageV2Upgrade.mjs.map +0 -1
- package/dist/durable/SqlSubscriptionStore.d.mts +0 -62
- package/dist/durable/SqlSubscriptionStore.mjs +0 -751
- package/dist/durable/SqlSubscriptionStore.mjs.map +0 -1
- package/dist/durable/SqlThreadNativeReads.d.mts +0 -55
- package/dist/durable/SqlThreadNativeReads.mjs +0 -196
- package/dist/durable/SqlThreadNativeReads.mjs.map +0 -1
- package/src/durable/SqlMessageDeliveryStore.ts +0 -335
- package/src/durable/SqlStorageV2Upgrade.ts +0 -500
- package/src/durable/SqlSubscriptionStore.ts +0 -1440
- package/src/durable/SqlThreadNativeReads.ts +0 -343
- package/src/durable/internal/storage-v2-layout.ts +0 -163
- package/src/durable/internal/storage-v2.ts +0 -73
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Schema } from "effect";
|
|
2
|
-
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
3
|
-
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
4
|
-
|
|
5
|
-
import { ThreadId } from "../core/Identifiers.ts";
|
|
6
|
-
import {
|
|
7
|
-
applyMessageDeliveryChange,
|
|
8
|
-
defaultMessageDeliveryStoreLimits,
|
|
9
|
-
MessageDeliveryChange,
|
|
10
|
-
MessageDeliveryError,
|
|
11
|
-
type MessageDeliveryFailure,
|
|
12
|
-
MessageDeliveryFailpoint,
|
|
13
|
-
MessageDeliveryKey,
|
|
14
|
-
MessageDeliveryPageRequest,
|
|
15
|
-
MessageDeliveryRecord,
|
|
16
|
-
MessageDeliveryStore,
|
|
17
|
-
MessageDeliveryStoreLimits,
|
|
18
|
-
messageDeliveryDeadline,
|
|
19
|
-
isWorkerUpdateDelivery,
|
|
20
|
-
messageDeliveryCapacity,
|
|
21
|
-
sameMessageDeliveryIdentity,
|
|
22
|
-
validateMessageDelivery,
|
|
23
|
-
} from "./MessageDelivery.ts";
|
|
24
|
-
import { ScheduleInstant } from "./Schedule.ts";
|
|
25
|
-
import { IdempotencyKey } from "./SubmissionLedger.ts";
|
|
26
|
-
|
|
27
|
-
/** The adapter owns the atomic transaction and any associated native wake/alarm update. */
|
|
28
|
-
export class SqlMessageDeliveryTransaction extends Context.Service<
|
|
29
|
-
SqlMessageDeliveryTransaction,
|
|
30
|
-
{
|
|
31
|
-
readonly run: <A>(
|
|
32
|
-
body: Effect.Effect<A, MessageDeliveryFailure>,
|
|
33
|
-
) => Effect.Effect<A, MessageDeliveryFailure>;
|
|
34
|
-
}
|
|
35
|
-
>()("@effect-agent/thread/SqlMessageDeliveryTransaction") {}
|
|
36
|
-
|
|
37
|
-
export interface SqlMessageDeliveryStoreOptions {
|
|
38
|
-
/** UTF-8 bound on the complete persisted record, including a processed Settlement. */
|
|
39
|
-
readonly maxStoredValueBytes?: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const Row = Schema.Struct({
|
|
43
|
-
owner_thread_id: ThreadId,
|
|
44
|
-
message_id: IdempotencyKey,
|
|
45
|
-
version: Schema.Int,
|
|
46
|
-
state: Schema.String,
|
|
47
|
-
deadline_at_millis: Schema.NullOr(ScheduleInstant),
|
|
48
|
-
record_json: Schema.String,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const Count = Schema.Struct({ retained: Schema.Natural, pending: Schema.Natural });
|
|
52
|
-
const Deadline = Schema.Struct({ deadline: Schema.NullOr(ScheduleInstant) });
|
|
53
|
-
|
|
54
|
-
const Scan = Schema.Struct({
|
|
55
|
-
nowMillis: ScheduleInstant,
|
|
56
|
-
limit: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(100)),
|
|
57
|
-
ownerThreadId: Schema.optionalKey(ThreadId),
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const codec = Schema.fromJsonString(MessageDeliveryRecord);
|
|
61
|
-
const bytes = (text: string): number => new TextEncoder().encode(text).byteLength;
|
|
62
|
-
|
|
63
|
-
const storage = (operation: string, cause?: unknown) =>
|
|
64
|
-
MessageDeliveryError.make({
|
|
65
|
-
reason: "storage",
|
|
66
|
-
operation,
|
|
67
|
-
...(cause === undefined ? {} : { cause }),
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const corrupt = (operation: string, cause?: unknown) =>
|
|
71
|
-
MessageDeliveryError.make({
|
|
72
|
-
reason: "corrupt",
|
|
73
|
-
operation,
|
|
74
|
-
...(cause === undefined ? {} : { cause }),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const query = <A>(operation: string, effect: Effect.Effect<A, SqlError>) =>
|
|
78
|
-
effect.pipe(Effect.mapError((cause) => storage(operation, cause)));
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Shared SQL implementation over the adapter-owned effect_agent_message_deliveries table.
|
|
82
|
-
* Required columns: owner_thread_id, message_id (composite primary key), version, state,
|
|
83
|
-
* deadline_at_millis (nullable effective wake deadline), record_json. Index non-null deadlines
|
|
84
|
-
* by (deadline_at_millis, owner_thread_id, message_id). No source/receiver ledger joins.
|
|
85
|
-
* Before failpoints precede the atomic transaction; after failpoints run after its commit.
|
|
86
|
-
*/
|
|
87
|
-
export const makeSqlMessageDeliveryStore = Effect.fn("SqlMessageDeliveryStore.make")(function* (
|
|
88
|
-
limits: MessageDeliveryStoreLimits = defaultMessageDeliveryStoreLimits,
|
|
89
|
-
options: SqlMessageDeliveryStoreOptions = {},
|
|
90
|
-
) {
|
|
91
|
-
const config = yield* validateMessageDelivery(MessageDeliveryStoreLimits, limits, "limits");
|
|
92
|
-
|
|
93
|
-
const maxStoredValueBytes = yield* validateMessageDelivery(
|
|
94
|
-
Schema.Int.check(Schema.isGreaterThan(0)),
|
|
95
|
-
options.maxStoredValueBytes ?? 16 * 1_024 * 1_024,
|
|
96
|
-
"stored-value-limit",
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const sql = yield* SqlClient.SqlClient;
|
|
100
|
-
const transaction = yield* SqlMessageDeliveryTransaction;
|
|
101
|
-
const failpoint = yield* MessageDeliveryFailpoint;
|
|
102
|
-
|
|
103
|
-
const encode = Effect.fn("SqlMessageDeliveryStore.encode")(function* (
|
|
104
|
-
record: MessageDeliveryRecord,
|
|
105
|
-
) {
|
|
106
|
-
const text = yield* Schema.encodeEffect(codec)(record).pipe(
|
|
107
|
-
Effect.mapError((cause) => corrupt("encode", cause)),
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
if (bytes(text) > maxStoredValueBytes)
|
|
111
|
-
return yield* MessageDeliveryError.make({
|
|
112
|
-
reason: "capacity",
|
|
113
|
-
operation: "stored-value-bytes",
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
return text;
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const decode = Effect.fn("SqlMessageDeliveryStore.decode")(function* (row: typeof Row.Type) {
|
|
120
|
-
if (bytes(row.record_json) > maxStoredValueBytes) return yield* corrupt("stored-value-bytes");
|
|
121
|
-
|
|
122
|
-
const record = yield* Schema.decodeEffect(codec)(row.record_json).pipe(
|
|
123
|
-
Effect.mapError((cause) => corrupt("decode", cause)),
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
record.key.ownerThreadId !== row.owner_thread_id ||
|
|
128
|
-
record.key.messageId !== row.message_id ||
|
|
129
|
-
record.version !== row.version ||
|
|
130
|
-
record.status !== row.state ||
|
|
131
|
-
messageDeliveryDeadline(record) !== row.deadline_at_millis
|
|
132
|
-
) {
|
|
133
|
-
return yield* corrupt("row-metadata");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return record;
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
const decodeRows = (rows: unknown) =>
|
|
140
|
-
Schema.decodeUnknownEffect(Schema.Array(Row))(rows).pipe(
|
|
141
|
-
Effect.mapError((cause) => corrupt("rows", cause)),
|
|
142
|
-
Effect.flatMap((rows) => Effect.forEach(rows, decode)),
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const get: MessageDeliveryStore["Service"]["get"] = Effect.fn("SqlMessageDeliveryStore.get")(
|
|
146
|
-
function* (key) {
|
|
147
|
-
const input = yield* validateMessageDelivery(MessageDeliveryKey, key, "get");
|
|
148
|
-
|
|
149
|
-
const rows = yield* query(
|
|
150
|
-
"get",
|
|
151
|
-
sql`SELECT owner_thread_id, message_id, version, state, deadline_at_millis, record_json FROM effect_agent_message_deliveries WHERE owner_thread_id = ${input.ownerThreadId} AND message_id = ${input.messageId}`,
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
return (yield* decodeRows(rows))[0] ?? null;
|
|
155
|
-
},
|
|
156
|
-
);
|
|
157
|
-
|
|
158
|
-
const insert: MessageDeliveryStore["Service"]["insert"] = Effect.fn(
|
|
159
|
-
"SqlMessageDeliveryStore.insert",
|
|
160
|
-
)(function* (record) {
|
|
161
|
-
const text = yield* encode(record);
|
|
162
|
-
|
|
163
|
-
const input = yield* Schema.decodeEffect(codec)(text).pipe(
|
|
164
|
-
Effect.mapError((cause) => corrupt("insert", cause)),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
if (
|
|
168
|
-
input.version !== 1 ||
|
|
169
|
-
input.status !== "pending" ||
|
|
170
|
-
input.leaseUntilMillis !== null ||
|
|
171
|
-
input.retry.attempts !== 0 ||
|
|
172
|
-
input.retry.generation !== 0 ||
|
|
173
|
-
input.retry.automaticAttempts !== 0 ||
|
|
174
|
-
input.retry.nextAttemptAtMillis !== input.createdAtMillis ||
|
|
175
|
-
input.retry.lastAttemptAtMillis !== null ||
|
|
176
|
-
input.retry.lastFailure !== null ||
|
|
177
|
-
input.deadlineAtMillis !== input.initialDeadlineAtMillis
|
|
178
|
-
) {
|
|
179
|
-
return yield* MessageDeliveryError.make({ reason: "validation", operation: "insert-state" });
|
|
180
|
-
}
|
|
181
|
-
if (bytes(JSON.stringify(input.envelope)) > config.maxEnvelopeBytes)
|
|
182
|
-
return yield* MessageDeliveryError.make({ reason: "capacity", operation: "envelope-bytes" });
|
|
183
|
-
yield* failpoint.hit("message-delivery:insert:before");
|
|
184
|
-
|
|
185
|
-
const result = yield* transaction.run(
|
|
186
|
-
Effect.gen(function* () {
|
|
187
|
-
const existing = yield* get(input.key);
|
|
188
|
-
|
|
189
|
-
if (existing !== null) {
|
|
190
|
-
if (!sameMessageDeliveryIdentity(existing, input))
|
|
191
|
-
return yield* MessageDeliveryError.make({ reason: "conflict", operation: "insert" });
|
|
192
|
-
|
|
193
|
-
return existing;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const update = isWorkerUpdateDelivery(input);
|
|
197
|
-
const capacity = messageDeliveryCapacity(config, update);
|
|
198
|
-
|
|
199
|
-
const counts = yield* query(
|
|
200
|
-
"count",
|
|
201
|
-
sql`SELECT COUNT(*) AS retained, COALESCE(SUM(CASE WHEN state IN ('pending', 'accepted', 'parked') THEN 1 ELSE 0 END), 0) AS pending FROM effect_agent_message_deliveries WHERE owner_thread_id = ${input.key.ownerThreadId} AND COALESCE(json_extract(record_json, '$.envelope.messageAdmission._tag'), '') ${update ? sql`= 'WorkerUpdate'` : sql`<> 'WorkerUpdate'`}`,
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
const count = (yield* Schema.decodeUnknownEffect(Schema.Array(Count))(counts).pipe(
|
|
205
|
-
Effect.mapError((cause) => corrupt("count", cause)),
|
|
206
|
-
))[0];
|
|
207
|
-
|
|
208
|
-
if (count === undefined) return yield* corrupt("count");
|
|
209
|
-
if (count.retained >= capacity.retained || count.pending >= capacity.pending)
|
|
210
|
-
return yield* MessageDeliveryError.make({ reason: "capacity", operation: "insert" });
|
|
211
|
-
yield* query(
|
|
212
|
-
"insert",
|
|
213
|
-
sql`INSERT INTO effect_agent_message_deliveries (owner_thread_id, message_id, version, state, deadline_at_millis, record_json) VALUES (${input.key.ownerThreadId}, ${input.key.messageId}, ${input.version}, ${input.status}, ${messageDeliveryDeadline(input)}, ${text})`,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
return input;
|
|
217
|
-
}),
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
yield* failpoint.hit("message-delivery:insert:after");
|
|
221
|
-
|
|
222
|
-
return result;
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const change: MessageDeliveryStore["Service"]["change"] = Effect.fn(
|
|
226
|
-
"SqlMessageDeliveryStore.change",
|
|
227
|
-
)(function* (key, change) {
|
|
228
|
-
const decodedKey = yield* validateMessageDelivery(MessageDeliveryKey, key, "change");
|
|
229
|
-
const input = yield* validateMessageDelivery(MessageDeliveryChange, change, "change");
|
|
230
|
-
const point = `message-delivery:${input._tag.toLowerCase()}`;
|
|
231
|
-
|
|
232
|
-
yield* failpoint.hit(`${point}:before`);
|
|
233
|
-
|
|
234
|
-
const result = yield* transaction.run(
|
|
235
|
-
Effect.gen(function* () {
|
|
236
|
-
const current = yield* get(decodedKey);
|
|
237
|
-
|
|
238
|
-
if (current === null)
|
|
239
|
-
return yield* MessageDeliveryError.make({ reason: "not-found", operation: "change" });
|
|
240
|
-
const next = yield* Effect.fromResult(applyMessageDeliveryChange(current, input));
|
|
241
|
-
const text = yield* encode(next);
|
|
242
|
-
|
|
243
|
-
const updated = yield* query(
|
|
244
|
-
"change",
|
|
245
|
-
sql`UPDATE effect_agent_message_deliveries SET version = ${next.version}, state = ${next.status}, deadline_at_millis = ${messageDeliveryDeadline(next)}, record_json = ${text} WHERE owner_thread_id = ${decodedKey.ownerThreadId} AND message_id = ${decodedKey.messageId} AND version = ${input.expectedVersion} RETURNING owner_thread_id, message_id, version, state, deadline_at_millis, record_json`,
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
const rows = yield* decodeRows(updated);
|
|
249
|
-
|
|
250
|
-
if (rows.length !== 1 || rows[0] === undefined)
|
|
251
|
-
return yield* MessageDeliveryError.make({ reason: "conflict", operation: "change" });
|
|
252
|
-
|
|
253
|
-
return rows[0];
|
|
254
|
-
}),
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
yield* failpoint.hit(`${point}:after`);
|
|
258
|
-
|
|
259
|
-
return result;
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
return MessageDeliveryStore.of({
|
|
263
|
-
limits: config,
|
|
264
|
-
maxStoredValueBytes,
|
|
265
|
-
insert,
|
|
266
|
-
get,
|
|
267
|
-
change,
|
|
268
|
-
list: Effect.fn("SqlMessageDeliveryStore.list")(function* (request) {
|
|
269
|
-
const input = yield* validateMessageDelivery(MessageDeliveryPageRequest, request, "list");
|
|
270
|
-
|
|
271
|
-
const rows = yield* query(
|
|
272
|
-
"list",
|
|
273
|
-
sql`SELECT owner_thread_id, message_id, version, state, deadline_at_millis, record_json FROM effect_agent_message_deliveries WHERE owner_thread_id = ${input.ownerThreadId} ${input.after === undefined ? sql`` : sql`AND message_id > ${input.after}`} ${input.pendingOnly ? sql`AND state NOT IN ('processed', 'refused')` : sql``}
|
|
274
|
-
${
|
|
275
|
-
input.workerStarts === undefined
|
|
276
|
-
? sql``
|
|
277
|
-
: sql`AND json_extract(record_json, '$.envelope.workerAdmission.origin.worker.delegationId') = ${input.workerStarts.delegationId}
|
|
278
|
-
AND json_extract(record_json, '$.envelope.workerAdmission.origin.worker.targetAgentId') = ${input.workerStarts.targetAgentId}
|
|
279
|
-
AND message_id = json_extract(record_json, '$.envelope.workerAdmission.origin.firstMessageId')`
|
|
280
|
-
}
|
|
281
|
-
${
|
|
282
|
-
input.pendingWorker === undefined
|
|
283
|
-
? sql``
|
|
284
|
-
: sql`AND json_extract(record_json, '$.envelope.workerAdmission.origin.worker.threadId') = ${input.pendingWorker}
|
|
285
|
-
AND state IN ('pending', 'parked') AND json_extract(record_json, '$.receipt') IS NULL`
|
|
286
|
-
}
|
|
287
|
-
ORDER BY message_id LIMIT ${input.limit + 1}`,
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
const records = yield* decodeRows(rows);
|
|
291
|
-
const items = records.slice(0, input.limit);
|
|
292
|
-
|
|
293
|
-
return {
|
|
294
|
-
items,
|
|
295
|
-
next: records.length > input.limit ? (items.at(-1)?.key.messageId ?? null) : null,
|
|
296
|
-
};
|
|
297
|
-
}),
|
|
298
|
-
due: Effect.fn("SqlMessageDeliveryStore.due")(function* (nowMillis, limit, ownerThreadId) {
|
|
299
|
-
const input = yield* validateMessageDelivery(
|
|
300
|
-
Scan,
|
|
301
|
-
{ nowMillis, limit, ...(ownerThreadId === undefined ? {} : { ownerThreadId }) },
|
|
302
|
-
"due",
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
const rows = yield* query(
|
|
306
|
-
"due",
|
|
307
|
-
sql`SELECT owner_thread_id, message_id, version, state, deadline_at_millis, record_json FROM effect_agent_message_deliveries WHERE deadline_at_millis IS NOT NULL AND deadline_at_millis <= ${input.nowMillis} ${input.ownerThreadId === undefined ? sql`` : sql`AND owner_thread_id = ${input.ownerThreadId}`} ORDER BY deadline_at_millis, owner_thread_id, message_id LIMIT ${input.limit}`,
|
|
308
|
-
);
|
|
309
|
-
|
|
310
|
-
return (yield* decodeRows(rows)).map((record) => record.key);
|
|
311
|
-
}),
|
|
312
|
-
nextDeadline: Effect.fn("SqlMessageDeliveryStore.nextDeadline")(function* (ownerThreadId) {
|
|
313
|
-
if (ownerThreadId !== undefined)
|
|
314
|
-
yield* validateMessageDelivery(ThreadId, ownerThreadId, "nextDeadline");
|
|
315
|
-
|
|
316
|
-
const rows = yield* query(
|
|
317
|
-
"next-deadline",
|
|
318
|
-
sql`SELECT MIN(deadline_at_millis) AS deadline FROM effect_agent_message_deliveries ${ownerThreadId === undefined ? sql`` : sql`WHERE owner_thread_id = ${ownerThreadId}`}`,
|
|
319
|
-
);
|
|
320
|
-
|
|
321
|
-
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(Deadline))(rows).pipe(
|
|
322
|
-
Effect.mapError((cause) => corrupt("next-deadline", cause)),
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
return decoded[0]?.deadline ?? null;
|
|
326
|
-
}),
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
/** Add the pending-only index within the adapter's format transaction. */
|
|
331
|
-
export const createMessageDeliveryPendingIndex = Effect.gen(function* () {
|
|
332
|
-
const sql = yield* SqlClient.SqlClient;
|
|
333
|
-
|
|
334
|
-
yield* sql`CREATE INDEX effect_agent_message_deliveries_pending ON effect_agent_message_deliveries(owner_thread_id, message_id) WHERE state NOT IN ('processed', 'refused')`;
|
|
335
|
-
});
|