effect-agent 0.1.0-beta.150 → 0.1.0-beta.151
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/{Admin-B8ISZyYf.d.mts → Admin-CKIdJoP8.d.mts} +6 -5
- package/dist/{DurableAgentRuntime-DLa4IwFi.mjs → DurableAgentRuntime-R2LUYnRl.mjs} +67 -81
- package/dist/DurableAgentRuntime-R2LUYnRl.mjs.map +1 -0
- package/dist/{EventSource-CkN05NT3.d.mts → EventSource-CMbXXmwe.d.mts} +2 -2
- package/dist/{PreparedInputAdmission-fctZOTnn.d.mts → PreparedInputAdmission-Bt6rgtVu.d.mts} +3 -3
- package/dist/{Subscription-B16A-48F.d.mts → Subscription-BJAzYNw1.d.mts} +2 -2
- package/dist/{SubscriptionInput-DIU7CiXE.d.mts → SubscriptionInput-CkBJt0AS.d.mts} +2 -2
- package/dist/WorkerAdmission-CJ_oWRuS.mjs +171 -0
- package/dist/WorkerAdmission-CJ_oWRuS.mjs.map +1 -0
- package/dist/durable/Admin.d.mts +1 -1
- package/dist/durable/AgentRegistration.d.mts +1 -1
- package/dist/durable/Certification.d.mts +1 -1
- package/dist/durable/CommittedActivity.d.mts +1 -1
- package/dist/durable/DurableAgentRuntime.d.mts +1 -1
- package/dist/durable/DurableAgentRuntime.mjs +2 -1
- package/dist/durable/EventSource.d.mts +1 -1
- package/dist/durable/GitHubWorkflowSource.d.mts +3 -3
- package/dist/durable/LifecyclePublication.d.mts +1 -1
- package/dist/durable/MessageDelivery.d.mts +41 -41
- package/dist/durable/MessageDeliveryStoreConformance.d.mts +39 -39
- package/dist/durable/PersistentHistory.d.mts +1 -1
- package/dist/durable/PreparedInputAdmission.d.mts +1 -1
- package/dist/durable/Recovery.d.mts +1 -1
- package/dist/durable/RunJournal.d.mts +1 -1
- package/dist/durable/Scheduling.d.mts +1 -1
- package/dist/durable/SubmissionLedger.d.mts +1 -1
- package/dist/durable/SubmissionLedgerConformance.d.mts +1 -1
- package/dist/durable/SubmissionStatus.d.mts +1 -1
- package/dist/durable/Subscription.d.mts +1 -1
- package/dist/durable/SubscriptionInput.d.mts +1 -1
- package/dist/durable/SubscriptionStoreConformance.d.mts +1 -1
- package/dist/durable/SubscriptionTools.d.mts +2 -2
- package/dist/durable/SubscriptionTransition.d.mts +1 -1
- package/dist/durable/Subscriptions.d.mts +5 -5
- package/dist/durable/ThreadContextHistory.d.mts +1 -1
- package/dist/durable/ThreadInvariants.d.mts +1 -1
- package/dist/durable/ThreadProjectionMaintenance.d.mts +1 -1
- package/dist/durable/ThreadStore.d.mts +1 -1
- package/dist/durable/ThreadStoreConformance.d.mts +1 -1
- package/dist/durable/WorkerAdmission.d.mts +172 -0
- package/dist/durable/WorkerAdmission.mjs +2 -0
- package/dist/durable/WorkerHost.d.mts +1 -1
- package/dist/index.d.mts +7 -6
- package/dist/index.mjs +3 -2
- package/package.json +1 -1
- package/src/durable/DurableAgentRuntime.ts +68 -108
- package/src/durable/WorkerAdmission.ts +104 -0
- package/src/durable/internal/thread-initialization.ts +195 -0
- package/src/durable/internal/worker-host.ts +121 -123
- package/src/index.ts +1 -0
- package/dist/DurableAgentRuntime-DLa4IwFi.mjs.map +0 -1
|
@@ -163,6 +163,7 @@ import {
|
|
|
163
163
|
} from "./internal/journal-checkpoint.ts";
|
|
164
164
|
import { makeJournalMetadata, type JournalMetadata } from "./internal/journal-metadata.ts";
|
|
165
165
|
import { makeMessagingRuntime } from "./internal/messaging-host.ts";
|
|
166
|
+
import * as ThreadInitialization from "./internal/thread-initialization.ts";
|
|
166
167
|
import { makeWorkerRuntime, WorkerInputControl } from "./internal/worker-host.ts";
|
|
167
168
|
import { WorkerRuntime } from "./internal/worker-runtime.ts";
|
|
168
169
|
import {
|
|
@@ -181,7 +182,6 @@ import {
|
|
|
181
182
|
CanonicalBatch,
|
|
182
183
|
CanonicalSequence,
|
|
183
184
|
CompactionCreated,
|
|
184
|
-
ThreadCreated,
|
|
185
185
|
DefinitionDigests,
|
|
186
186
|
ModelResponseInterrupted,
|
|
187
187
|
PersistedJson,
|
|
@@ -357,6 +357,7 @@ import {
|
|
|
357
357
|
} from "./ThreadStore.ts";
|
|
358
358
|
import { PreparedToolCallEvidence, ToolReconciler } from "./ToolReconciler.ts";
|
|
359
359
|
import { WakeScheduler } from "./WakeScheduler.ts";
|
|
360
|
+
import { WorkerAdmissionPort, WorkerAdmissionRequest } from "./WorkerAdmission.ts";
|
|
360
361
|
|
|
361
362
|
// Capture the tracing call site once; each application still creates a fresh Attempt span.
|
|
362
363
|
const withThreadHeadSpan = Effect.withSpan("DurableAgentRuntime.processThreadHead");
|
|
@@ -536,13 +537,7 @@ const childUsageReportsOf = (state: SubagentCallRecords): ReadonlyArray<ChildRun
|
|
|
536
537
|
});
|
|
537
538
|
});
|
|
538
539
|
|
|
539
|
-
|
|
540
|
-
export const threadCreatedBatchId = (threadId: ThreadId): BatchId =>
|
|
541
|
-
decodeBatchId(`thread-created:${threadId}`);
|
|
542
|
-
|
|
543
|
-
/** Deterministic record identity of one Thread's `ThreadCreated` record. */
|
|
544
|
-
export const threadCreatedRecordId = (threadId: ThreadId): RecordId =>
|
|
545
|
-
decodeRecordId(`thread-created:${threadId}`);
|
|
540
|
+
export { threadCreatedBatchId, threadCreatedRecordId } from "./internal/thread-initialization.ts";
|
|
546
541
|
|
|
547
542
|
/** Deterministic batch identity of one executed recovery decision's audit append (DUR-013). */
|
|
548
543
|
export const recoveryRepairBatchId = (submissionId: SubmissionId, decisionTag: string): BatchId =>
|
|
@@ -1262,6 +1257,7 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
|
|
|
1262
1257
|
bindings: ReadonlyArray<ResolvedBinding>,
|
|
1263
1258
|
) {
|
|
1264
1259
|
const registeredBindings = [...bindings];
|
|
1260
|
+
const workerAdmissionPort = yield* WorkerAdmissionPort;
|
|
1265
1261
|
const ledger = yield* SubmissionLedger;
|
|
1266
1262
|
const submissionScheduling = yield* SubmissionScheduling;
|
|
1267
1263
|
const store = yield* ThreadStore;
|
|
@@ -2380,67 +2376,17 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
|
|
|
2380
2376
|
.materialize(ThreadMaterialization.make({ threadId, producerEpoch }))
|
|
2381
2377
|
.pipe(Effect.catchTag("FenceRejected", () => Effect.void));
|
|
2382
2378
|
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
definitions: DefinitionDigests,
|
|
2395
|
-
): Effect.fn.Return<
|
|
2396
|
-
void,
|
|
2397
|
-
ThreadStoreError | ThreadNotMaterialized | AppendConflict | FenceRejected
|
|
2398
|
-
> {
|
|
2399
|
-
const tail = yield* store.inspectTail(ThreadTailRequest.make({ threadId }));
|
|
2400
|
-
|
|
2401
|
-
if (tail.tailSequence > 0) return;
|
|
2402
|
-
|
|
2403
|
-
const record = yield* makeEnvelope(
|
|
2404
|
-
threadCreatedRecordId(threadId),
|
|
2405
|
-
ThreadCreated.make({ agentId, definitions }),
|
|
2406
|
-
);
|
|
2407
|
-
|
|
2408
|
-
yield* store
|
|
2409
|
-
.append(
|
|
2410
|
-
FencedAppendRequest.make({
|
|
2411
|
-
threadId,
|
|
2412
|
-
batch: CanonicalBatch.make({
|
|
2413
|
-
batchId: threadCreatedBatchId(threadId),
|
|
2414
|
-
producerId: config.producerId,
|
|
2415
|
-
records: [record],
|
|
2416
|
-
}),
|
|
2417
|
-
expectedTailSequence: tail.tailSequence,
|
|
2418
|
-
expectedTailDigest: tail.tailDigest,
|
|
2419
|
-
producerEpoch: tail.producerEpoch,
|
|
2420
|
-
}),
|
|
2421
|
-
)
|
|
2422
|
-
.pipe(
|
|
2423
|
-
Effect.catchTag(["AppendConflict", "FenceRejected"], (error) =>
|
|
2424
|
-
store
|
|
2425
|
-
.inspectTail(ThreadTailRequest.make({ threadId }))
|
|
2426
|
-
.pipe(
|
|
2427
|
-
Effect.flatMap((current) =>
|
|
2428
|
-
current.tailSequence > 0 ? Effect.void : Effect.fail(error),
|
|
2429
|
-
),
|
|
2430
|
-
),
|
|
2431
|
-
),
|
|
2432
|
-
Effect.andThen(wake.notify(threadId)),
|
|
2433
|
-
Effect.asVoid,
|
|
2434
|
-
);
|
|
2435
|
-
},
|
|
2436
|
-
(effect) =>
|
|
2437
|
-
effect.pipe(
|
|
2438
|
-
Effect.retry({
|
|
2439
|
-
times: 7,
|
|
2440
|
-
while: (error) => error._tag === "AppendConflict" || error._tag === "FenceRejected",
|
|
2441
|
-
}),
|
|
2442
|
-
),
|
|
2443
|
-
);
|
|
2379
|
+
const ensureThreadCreated = (
|
|
2380
|
+
threadId: ThreadId,
|
|
2381
|
+
agentId: AgentId,
|
|
2382
|
+
definitions: DefinitionDigests,
|
|
2383
|
+
) =>
|
|
2384
|
+
ThreadInitialization.ensureThreadCreated(
|
|
2385
|
+
{ producerId: config.producerId, deploymentId: config.deploymentId },
|
|
2386
|
+
threadId,
|
|
2387
|
+
agentId,
|
|
2388
|
+
definitions,
|
|
2389
|
+
).pipe(Effect.provideService(ThreadStore, store), Effect.provideService(WakeScheduler, wake));
|
|
2444
2390
|
|
|
2445
2391
|
const attemptContextFor = Effect.fn("DurableAgentRuntime.attemptContextFor")(function* (
|
|
2446
2392
|
threadId: ThreadId,
|
|
@@ -10109,41 +10055,64 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
|
|
|
10109
10055
|
});
|
|
10110
10056
|
}
|
|
10111
10057
|
|
|
10112
|
-
const
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
}),
|
|
10133
|
-
),
|
|
10058
|
+
const request = yield* Schema.decodeEffect(AdmissionRequest)({
|
|
10059
|
+
threadId: options.threadId,
|
|
10060
|
+
principal: options.principal,
|
|
10061
|
+
idempotencyKey: options.idempotencyKey,
|
|
10062
|
+
...(options.admissionGroup === undefined ? {} : { admissionGroup: options.admissionGroup }),
|
|
10063
|
+
...(options.admissionFence === undefined ? {} : { admissionFence: options.admissionFence }),
|
|
10064
|
+
agentId: agent.definition.id,
|
|
10065
|
+
agentDigests: options.definitions,
|
|
10066
|
+
deploymentId: config.deploymentId,
|
|
10067
|
+
inputPayload,
|
|
10068
|
+
inputDigest,
|
|
10069
|
+
...(workerAdmission === undefined
|
|
10070
|
+
? {}
|
|
10071
|
+
: {
|
|
10072
|
+
workerAdmission: yield* Schema.encodeEffect(WorkerAdmission)(workerAdmission).pipe(
|
|
10073
|
+
Effect.mapError(() =>
|
|
10074
|
+
LedgerError.make({
|
|
10075
|
+
operation: "submit",
|
|
10076
|
+
message: "Worker admission cannot be encoded",
|
|
10077
|
+
}),
|
|
10134
10078
|
),
|
|
10135
|
-
|
|
10136
|
-
...(messageAdmission === undefined ? {} : { messageAdmission }),
|
|
10137
|
-
}).pipe(
|
|
10138
|
-
Effect.mapError(() =>
|
|
10139
|
-
LedgerError.make({
|
|
10140
|
-
operation: "submit",
|
|
10141
|
-
message: "Admission fields do not satisfy the ledger contract",
|
|
10079
|
+
),
|
|
10142
10080
|
}),
|
|
10143
|
-
|
|
10081
|
+
...(messageAdmission === undefined ? {} : { messageAdmission }),
|
|
10082
|
+
}).pipe(
|
|
10083
|
+
Effect.mapError(() =>
|
|
10084
|
+
LedgerError.make({
|
|
10085
|
+
operation: "submit",
|
|
10086
|
+
message: "Admission fields do not satisfy the ledger contract",
|
|
10087
|
+
}),
|
|
10144
10088
|
),
|
|
10145
10089
|
);
|
|
10146
10090
|
|
|
10091
|
+
if (workerAdmission !== undefined) {
|
|
10092
|
+
const admitted = yield* workerAdmissionPort
|
|
10093
|
+
.admit(
|
|
10094
|
+
WorkerAdmissionRequest.make({
|
|
10095
|
+
...request,
|
|
10096
|
+
workerAdmission,
|
|
10097
|
+
producerId: config.producerId,
|
|
10098
|
+
}),
|
|
10099
|
+
)
|
|
10100
|
+
.pipe(
|
|
10101
|
+
Effect.provideService(SubmissionLedger, ledger),
|
|
10102
|
+
Effect.provideService(ThreadStore, store),
|
|
10103
|
+
Effect.provideService(WakeScheduler, wake),
|
|
10104
|
+
Effect.provideService(DurableRuntimeFailpoint, failpoint),
|
|
10105
|
+
);
|
|
10106
|
+
|
|
10107
|
+
return Receipt.make({
|
|
10108
|
+
receiptId: admitted.receiptId,
|
|
10109
|
+
submissionId: admitted.submissionId,
|
|
10110
|
+
threadId: request.threadId,
|
|
10111
|
+
queueSequence: admitted.queueSequence,
|
|
10112
|
+
});
|
|
10113
|
+
}
|
|
10114
|
+
const admitted = yield* ledger.admit(request);
|
|
10115
|
+
|
|
10147
10116
|
yield* hit("submit:after-admit");
|
|
10148
10117
|
|
|
10149
10118
|
const receipt = Receipt.make({
|
|
@@ -10160,15 +10129,6 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
|
|
|
10160
10129
|
}
|
|
10161
10130
|
yield* materializeAtLeast(options.threadId, ZERO_EPOCH);
|
|
10162
10131
|
yield* ensureThreadCreated(options.threadId, agent.definition.id, options.definitions);
|
|
10163
|
-
if (workerAdmission !== undefined) {
|
|
10164
|
-
yield* workerRuntime
|
|
10165
|
-
.ensureOrigin(workerAdmission.origin)
|
|
10166
|
-
.pipe(
|
|
10167
|
-
Effect.mapError((cause) =>
|
|
10168
|
-
LedgerError.make({ operation: "worker-origin", message: cause.reason, cause }),
|
|
10169
|
-
),
|
|
10170
|
-
);
|
|
10171
|
-
}
|
|
10172
10132
|
yield* hit("submit:after-materialize");
|
|
10173
10133
|
yield* ledger.markReady(MarkReadyRequest.make({ submissionId: admitted.submissionId }));
|
|
10174
10134
|
yield* wake.notify(options.threadId);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Context, Effect, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import { DurableRuntimeFailpoint, DurableRuntimeFailpointError } from "./DurableFailpoint.ts";
|
|
4
|
+
import { ensureThreadCreated, ensureWorkerOrigin } from "./internal/thread-initialization.ts";
|
|
5
|
+
import { ProducerEpoch, ProducerId, WorkerAdmission } from "./Records.ts";
|
|
6
|
+
import {
|
|
7
|
+
AdmissionConflict,
|
|
8
|
+
AdmissionPolicyError,
|
|
9
|
+
AdmissionRequest,
|
|
10
|
+
LedgerError,
|
|
11
|
+
MarkReadyRequest,
|
|
12
|
+
SubmissionLedger,
|
|
13
|
+
} from "./SubmissionLedger.ts";
|
|
14
|
+
import {
|
|
15
|
+
AppendConflict,
|
|
16
|
+
FenceRejected,
|
|
17
|
+
ThreadMaterialization,
|
|
18
|
+
ThreadNotMaterialized,
|
|
19
|
+
ThreadStore,
|
|
20
|
+
ThreadStoreError,
|
|
21
|
+
} from "./ThreadStore.ts";
|
|
22
|
+
import { WakeScheduler } from "./WakeScheduler.ts";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Storage admission after source authorization, input validation and capacity reservation.
|
|
26
|
+
* This is an internal host port, not an authorization entrypoint. Hosts route the whole
|
|
27
|
+
* operation to the destination owner and enroll recovery before executing it.
|
|
28
|
+
*/
|
|
29
|
+
export class WorkerAdmissionRequest extends Schema.Class<WorkerAdmissionRequest>(
|
|
30
|
+
"@effect-agent/thread/WorkerAdmissionRequest",
|
|
31
|
+
)({
|
|
32
|
+
...AdmissionRequest.fields,
|
|
33
|
+
workerAdmission: WorkerAdmission,
|
|
34
|
+
producerId: ProducerId,
|
|
35
|
+
}) {}
|
|
36
|
+
|
|
37
|
+
export const WorkerAdmissionFailure = Schema.Union([
|
|
38
|
+
AdmissionConflict,
|
|
39
|
+
AdmissionPolicyError,
|
|
40
|
+
LedgerError,
|
|
41
|
+
ThreadStoreError,
|
|
42
|
+
ThreadNotMaterialized,
|
|
43
|
+
AppendConflict,
|
|
44
|
+
FenceRejected,
|
|
45
|
+
DurableRuntimeFailpointError,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** Local implementation shared by every host; retries finish the same admitted row. */
|
|
49
|
+
export const admitWorker = Effect.fn("WorkerAdmission.admitWorker")(function* (
|
|
50
|
+
request: WorkerAdmissionRequest,
|
|
51
|
+
) {
|
|
52
|
+
const origin = request.workerAdmission.origin;
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
origin.worker.threadId !== request.threadId ||
|
|
56
|
+
origin.worker.targetAgentId !== request.agentId ||
|
|
57
|
+
request.parentLinkage !== undefined
|
|
58
|
+
)
|
|
59
|
+
return yield* LedgerError.make({
|
|
60
|
+
operation: "admit worker",
|
|
61
|
+
message: "Worker admission does not belong to the destination Thread",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const ledger = yield* SubmissionLedger;
|
|
65
|
+
const store = yield* ThreadStore;
|
|
66
|
+
const wake = yield* WakeScheduler;
|
|
67
|
+
const failpoint = yield* DurableRuntimeFailpoint;
|
|
68
|
+
const admitted = yield* ledger.admit(AdmissionRequest.make(request));
|
|
69
|
+
|
|
70
|
+
yield* failpoint.hit("submit:after-admit");
|
|
71
|
+
if (admitted.replayed && admitted.state !== "admitted") return admitted;
|
|
72
|
+
|
|
73
|
+
yield* store
|
|
74
|
+
.materialize(
|
|
75
|
+
ThreadMaterialization.make({
|
|
76
|
+
threadId: request.threadId,
|
|
77
|
+
producerEpoch: Schema.decodeSync(ProducerEpoch)(0),
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
.pipe(Effect.catchTag("FenceRejected", () => Effect.void));
|
|
81
|
+
|
|
82
|
+
const config = {
|
|
83
|
+
producerId: request.producerId,
|
|
84
|
+
deploymentId: request.deploymentId,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
yield* ensureThreadCreated(config, request.threadId, request.agentId, request.agentDigests);
|
|
88
|
+
yield* ensureWorkerOrigin(config, origin).pipe(
|
|
89
|
+
Effect.mapError((cause) =>
|
|
90
|
+
LedgerError.make({ operation: "worker-origin", message: cause.reason, cause }),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
yield* failpoint.hit("submit:after-materialize");
|
|
94
|
+
yield* ledger.markReady(MarkReadyRequest.make({ submissionId: admitted.submissionId }));
|
|
95
|
+
yield* wake.notify(request.threadId);
|
|
96
|
+
|
|
97
|
+
return admitted;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** Placement adapter for the composite operation; local hosts use the same implementation. */
|
|
101
|
+
export const WorkerAdmissionPort = Context.Reference<{ readonly admit: typeof admitWorker }>(
|
|
102
|
+
"@effect-agent/thread/WorkerAdmissionPort",
|
|
103
|
+
{ defaultValue: () => ({ admit: admitWorker }) },
|
|
104
|
+
);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { DateTime, Effect, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { AgentId, ThreadId } from "../../core/Identifiers.ts";
|
|
4
|
+
import { WorkerError } from "../../core/Worker.ts";
|
|
5
|
+
import { DurableRuntimeFailpoint } from "../DurableFailpoint.ts";
|
|
6
|
+
import {
|
|
7
|
+
BatchId,
|
|
8
|
+
CanonicalBatch,
|
|
9
|
+
RecordEnvelope,
|
|
10
|
+
RecordId,
|
|
11
|
+
ThreadCreated,
|
|
12
|
+
WorkerOrigin,
|
|
13
|
+
WorkerOriginRecorded,
|
|
14
|
+
type DefinitionDigests,
|
|
15
|
+
type DeploymentId,
|
|
16
|
+
type ProducerId,
|
|
17
|
+
} from "../Records.ts";
|
|
18
|
+
import { workerOriginRecordId } from "../RunJournal.ts";
|
|
19
|
+
import type {
|
|
20
|
+
AppendConflict,
|
|
21
|
+
FenceRejected,
|
|
22
|
+
ThreadNotMaterialized,
|
|
23
|
+
ThreadStoreError,
|
|
24
|
+
} from "../ThreadStore.ts";
|
|
25
|
+
import {
|
|
26
|
+
FencedAppendRequest,
|
|
27
|
+
ThreadIdentityRequest,
|
|
28
|
+
ThreadTailRequest,
|
|
29
|
+
ThreadStore,
|
|
30
|
+
} from "../ThreadStore.ts";
|
|
31
|
+
import { WakeScheduler } from "../WakeScheduler.ts";
|
|
32
|
+
import { definitionDigestsEqual } from "./agent-registration.ts";
|
|
33
|
+
|
|
34
|
+
interface InitializationConfig {
|
|
35
|
+
readonly producerId: ProducerId;
|
|
36
|
+
readonly deploymentId: DeploymentId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Stable identities retained across admission retries and recovery. */
|
|
40
|
+
export const threadCreatedBatchId = (threadId: ThreadId): BatchId =>
|
|
41
|
+
Schema.decodeSync(BatchId)(`thread-created:${threadId}`);
|
|
42
|
+
|
|
43
|
+
export const threadCreatedRecordId = (threadId: ThreadId): RecordId =>
|
|
44
|
+
Schema.decodeSync(RecordId)(`thread-created:${threadId}`);
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Coordinator invariant: every Thread's first canonical record is `ThreadCreated`,
|
|
48
|
+
* so `tailSequence >= 1` is the deterministic already-created check. A lost race (conflict or
|
|
49
|
+
* fence) is verified against that invariant instead of being trusted blindly. An admitted
|
|
50
|
+
* child can be claimed before it is runnable; that advances the shared storage fence without
|
|
51
|
+
* appending anything. Re-read and retry that initialization race with the current fence.
|
|
52
|
+
*/
|
|
53
|
+
export const ensureThreadCreated = Effect.fn("DurableAgentRuntime.ensureThreadCreated")(
|
|
54
|
+
function* (
|
|
55
|
+
config: InitializationConfig,
|
|
56
|
+
threadId: ThreadId,
|
|
57
|
+
agentId: AgentId,
|
|
58
|
+
definitions: DefinitionDigests,
|
|
59
|
+
): Effect.fn.Return<
|
|
60
|
+
void,
|
|
61
|
+
ThreadStoreError | ThreadNotMaterialized | AppendConflict | FenceRejected,
|
|
62
|
+
ThreadStore | WakeScheduler
|
|
63
|
+
> {
|
|
64
|
+
const store = yield* ThreadStore;
|
|
65
|
+
const wake = yield* WakeScheduler;
|
|
66
|
+
const tail = yield* store.inspectTail(ThreadTailRequest.make({ threadId }));
|
|
67
|
+
|
|
68
|
+
if (tail.tailSequence > 0) return;
|
|
69
|
+
|
|
70
|
+
const record = yield* Effect.map(DateTime.now, (createdAt) =>
|
|
71
|
+
RecordEnvelope.make({
|
|
72
|
+
recordId: threadCreatedRecordId(threadId),
|
|
73
|
+
family: "thread",
|
|
74
|
+
schemaVersion: 1,
|
|
75
|
+
createdAt,
|
|
76
|
+
deploymentId: config.deploymentId,
|
|
77
|
+
payload: ThreadCreated.make({ agentId, definitions }),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
yield* store
|
|
82
|
+
.append(
|
|
83
|
+
FencedAppendRequest.make({
|
|
84
|
+
threadId,
|
|
85
|
+
batch: CanonicalBatch.make({
|
|
86
|
+
batchId: threadCreatedBatchId(threadId),
|
|
87
|
+
producerId: config.producerId,
|
|
88
|
+
records: [record],
|
|
89
|
+
}),
|
|
90
|
+
expectedTailSequence: tail.tailSequence,
|
|
91
|
+
expectedTailDigest: tail.tailDigest,
|
|
92
|
+
producerEpoch: tail.producerEpoch,
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
.pipe(
|
|
96
|
+
Effect.catchTag(["AppendConflict", "FenceRejected"], (error) =>
|
|
97
|
+
store
|
|
98
|
+
.inspectTail(ThreadTailRequest.make({ threadId }))
|
|
99
|
+
.pipe(
|
|
100
|
+
Effect.flatMap((current) =>
|
|
101
|
+
current.tailSequence > 0 ? Effect.void : Effect.fail(error),
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
Effect.andThen(wake.notify(threadId)),
|
|
106
|
+
Effect.asVoid,
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
(effect) =>
|
|
110
|
+
effect.pipe(
|
|
111
|
+
Effect.retry({
|
|
112
|
+
times: 7,
|
|
113
|
+
while: (error) => error._tag === "AppendConflict" || error._tag === "FenceRejected",
|
|
114
|
+
}),
|
|
115
|
+
),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const sameOrigin = Schema.toEquivalence(WorkerOrigin);
|
|
119
|
+
|
|
120
|
+
export const ensureWorkerOrigin = Effect.fn("WorkerHost.ensureOrigin")(
|
|
121
|
+
function* (config: InitializationConfig, origin: WorkerOrigin) {
|
|
122
|
+
const store = yield* ThreadStore;
|
|
123
|
+
const failpoint = yield* DurableRuntimeFailpoint;
|
|
124
|
+
|
|
125
|
+
for (let attempt = 0; attempt < 16; attempt++) {
|
|
126
|
+
const current = yield* store.readIdentity(
|
|
127
|
+
ThreadIdentityRequest.make({ threadId: origin.worker.threadId }),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const existing = current.records.find(
|
|
131
|
+
({ record }) => record.payload._tag === "WorkerOriginRecorded",
|
|
132
|
+
)?.record.payload;
|
|
133
|
+
|
|
134
|
+
if (existing?._tag === "WorkerOriginRecorded") {
|
|
135
|
+
if (!sameOrigin(existing.origin, origin))
|
|
136
|
+
return yield* WorkerError.make({ operation: "start", reason: "worker-mismatch" });
|
|
137
|
+
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const first = current.records[0]?.record.payload;
|
|
141
|
+
|
|
142
|
+
if (
|
|
143
|
+
first?._tag !== "ThreadCreated" ||
|
|
144
|
+
first.agentId !== origin.worker.targetAgentId ||
|
|
145
|
+
!definitionDigestsEqual(first.definitions, origin.targetDigests) ||
|
|
146
|
+
current.records.some(({ record }) => record.payload._tag === "SubagentLineageRecorded")
|
|
147
|
+
)
|
|
148
|
+
return yield* WorkerError.make({ operation: "start", reason: "worker-mismatch" });
|
|
149
|
+
const id = workerOriginRecordId(origin.worker.threadId);
|
|
150
|
+
|
|
151
|
+
yield* failpoint.hit("worker:before-origin-append");
|
|
152
|
+
|
|
153
|
+
const appended = yield* store
|
|
154
|
+
.append(
|
|
155
|
+
FencedAppendRequest.make({
|
|
156
|
+
threadId: origin.worker.threadId,
|
|
157
|
+
producerEpoch: current.producerEpoch,
|
|
158
|
+
expectedTailSequence: current.tailSequence,
|
|
159
|
+
expectedTailDigest: current.tailDigest,
|
|
160
|
+
batch: CanonicalBatch.make({
|
|
161
|
+
batchId: Schema.decodeSync(BatchId)(id),
|
|
162
|
+
producerId: config.producerId,
|
|
163
|
+
records: [
|
|
164
|
+
RecordEnvelope.make({
|
|
165
|
+
recordId: id,
|
|
166
|
+
family: "thread",
|
|
167
|
+
schemaVersion: 1,
|
|
168
|
+
createdAt: DateTime.makeUnsafe(origin.createdAtMillis),
|
|
169
|
+
deploymentId: config.deploymentId,
|
|
170
|
+
payload: WorkerOriginRecorded.make({ origin }),
|
|
171
|
+
}),
|
|
172
|
+
],
|
|
173
|
+
}),
|
|
174
|
+
}),
|
|
175
|
+
)
|
|
176
|
+
.pipe(
|
|
177
|
+
Effect.as(true),
|
|
178
|
+
Effect.catchTag(["AppendConflict", "FenceRejected"], () => Effect.succeed(false)),
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (appended) {
|
|
182
|
+
yield* failpoint.hit("worker:after-origin-append");
|
|
183
|
+
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return yield* WorkerError.make({ operation: "start", reason: "storage" });
|
|
189
|
+
},
|
|
190
|
+
Effect.mapError((cause) =>
|
|
191
|
+
cause._tag === "WorkerError"
|
|
192
|
+
? cause
|
|
193
|
+
: WorkerError.make({ operation: "start", reason: "storage", cause }),
|
|
194
|
+
),
|
|
195
|
+
);
|