effect-agent 0.1.0-beta.150 → 0.1.0-beta.152
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-DQFV26SB.mjs} +68 -82
- package/dist/DurableAgentRuntime-DQFV26SB.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 +1 -1
- 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 +79 -109
- 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,
|
|
@@ -7068,7 +7014,17 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
|
|
|
7068
7014
|
: {}),
|
|
7069
7015
|
approval,
|
|
7070
7016
|
toolAuthorization,
|
|
7071
|
-
...(journal.toolSelection === undefined
|
|
7017
|
+
...(journal.toolSelection === undefined
|
|
7018
|
+
? {}
|
|
7019
|
+
: {
|
|
7020
|
+
toolSelection: {
|
|
7021
|
+
// A settled discovery receipt survives deployment; its selection only
|
|
7022
|
+
// names current tools. Keep validation of fresh selections fail-closed.
|
|
7023
|
+
toolNames: journal.toolSelection.toolNames.filter((name) =>
|
|
7024
|
+
Object.hasOwn(agent.definition.toolkit.tools, name),
|
|
7025
|
+
),
|
|
7026
|
+
},
|
|
7027
|
+
}),
|
|
7072
7028
|
durability,
|
|
7073
7029
|
subagent,
|
|
7074
7030
|
delegationDepth,
|
|
@@ -10109,41 +10065,64 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
|
|
|
10109
10065
|
});
|
|
10110
10066
|
}
|
|
10111
10067
|
|
|
10112
|
-
const
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
}),
|
|
10133
|
-
),
|
|
10068
|
+
const request = yield* Schema.decodeEffect(AdmissionRequest)({
|
|
10069
|
+
threadId: options.threadId,
|
|
10070
|
+
principal: options.principal,
|
|
10071
|
+
idempotencyKey: options.idempotencyKey,
|
|
10072
|
+
...(options.admissionGroup === undefined ? {} : { admissionGroup: options.admissionGroup }),
|
|
10073
|
+
...(options.admissionFence === undefined ? {} : { admissionFence: options.admissionFence }),
|
|
10074
|
+
agentId: agent.definition.id,
|
|
10075
|
+
agentDigests: options.definitions,
|
|
10076
|
+
deploymentId: config.deploymentId,
|
|
10077
|
+
inputPayload,
|
|
10078
|
+
inputDigest,
|
|
10079
|
+
...(workerAdmission === undefined
|
|
10080
|
+
? {}
|
|
10081
|
+
: {
|
|
10082
|
+
workerAdmission: yield* Schema.encodeEffect(WorkerAdmission)(workerAdmission).pipe(
|
|
10083
|
+
Effect.mapError(() =>
|
|
10084
|
+
LedgerError.make({
|
|
10085
|
+
operation: "submit",
|
|
10086
|
+
message: "Worker admission cannot be encoded",
|
|
10087
|
+
}),
|
|
10134
10088
|
),
|
|
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",
|
|
10089
|
+
),
|
|
10142
10090
|
}),
|
|
10143
|
-
|
|
10091
|
+
...(messageAdmission === undefined ? {} : { messageAdmission }),
|
|
10092
|
+
}).pipe(
|
|
10093
|
+
Effect.mapError(() =>
|
|
10094
|
+
LedgerError.make({
|
|
10095
|
+
operation: "submit",
|
|
10096
|
+
message: "Admission fields do not satisfy the ledger contract",
|
|
10097
|
+
}),
|
|
10144
10098
|
),
|
|
10145
10099
|
);
|
|
10146
10100
|
|
|
10101
|
+
if (workerAdmission !== undefined) {
|
|
10102
|
+
const admitted = yield* workerAdmissionPort
|
|
10103
|
+
.admit(
|
|
10104
|
+
WorkerAdmissionRequest.make({
|
|
10105
|
+
...request,
|
|
10106
|
+
workerAdmission,
|
|
10107
|
+
producerId: config.producerId,
|
|
10108
|
+
}),
|
|
10109
|
+
)
|
|
10110
|
+
.pipe(
|
|
10111
|
+
Effect.provideService(SubmissionLedger, ledger),
|
|
10112
|
+
Effect.provideService(ThreadStore, store),
|
|
10113
|
+
Effect.provideService(WakeScheduler, wake),
|
|
10114
|
+
Effect.provideService(DurableRuntimeFailpoint, failpoint),
|
|
10115
|
+
);
|
|
10116
|
+
|
|
10117
|
+
return Receipt.make({
|
|
10118
|
+
receiptId: admitted.receiptId,
|
|
10119
|
+
submissionId: admitted.submissionId,
|
|
10120
|
+
threadId: request.threadId,
|
|
10121
|
+
queueSequence: admitted.queueSequence,
|
|
10122
|
+
});
|
|
10123
|
+
}
|
|
10124
|
+
const admitted = yield* ledger.admit(request);
|
|
10125
|
+
|
|
10147
10126
|
yield* hit("submit:after-admit");
|
|
10148
10127
|
|
|
10149
10128
|
const receipt = Receipt.make({
|
|
@@ -10160,15 +10139,6 @@ const make = Effect.fn("DurableAgentRuntime.make")(function* (
|
|
|
10160
10139
|
}
|
|
10161
10140
|
yield* materializeAtLeast(options.threadId, ZERO_EPOCH);
|
|
10162
10141
|
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
10142
|
yield* hit("submit:after-materialize");
|
|
10173
10143
|
yield* ledger.markReady(MarkReadyRequest.make({ submissionId: admitted.submissionId }));
|
|
10174
10144
|
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
|
+
);
|