@stackstackstack/dsh-goal 0.1.5
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +58 -0
- package/README.zh.md +58 -0
- package/lib/index.js +827 -0
- package/lib/invariant.js +332 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +815 -0
- package/lib/typert.remote-client.d.ts +41 -0
- package/lib/typert.remote-client.js +366 -0
- package/lib/types/client.d.ts +10 -0
- package/lib/types/client.js +10 -0
- package/lib/types/domain.d.ts +92 -0
- package/lib/types/domain.js +10 -0
- package/lib/types/fold.d.ts +50 -0
- package/lib/types/fold.js +322 -0
- package/lib/types/index.d.ts +155 -0
- package/lib/types/index.js +495 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/invariant.js +70 -0
- package/lib/types/runtime.d.ts +21 -0
- package/lib/types/runtime.js +25 -0
- package/lib/types/types.d.ts +96 -0
- package/lib/types/types.js +13 -0
- package/package.json +82 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-session goal domain: event-sourced state, compare-and-set mutations,
|
|
3
|
+
* and process-local continuation activation.
|
|
4
|
+
* @module @stackstackstack/dsh-goal
|
|
5
|
+
*/
|
|
6
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
7
|
+
var useValue = arguments.length > 2;
|
|
8
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
9
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
10
|
+
}
|
|
11
|
+
return useValue ? value : void 0;
|
|
12
|
+
};
|
|
13
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
14
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
15
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
16
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
17
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
18
|
+
var _, done = false;
|
|
19
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
20
|
+
var context = {};
|
|
21
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
22
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
23
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
24
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
25
|
+
if (kind === "accessor") {
|
|
26
|
+
if (result === void 0) continue;
|
|
27
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
28
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
29
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
30
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
31
|
+
}
|
|
32
|
+
else if (_ = accept(result)) {
|
|
33
|
+
if (kind === "field") initializers.unshift(_);
|
|
34
|
+
else descriptor[key] = _;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
38
|
+
done = true;
|
|
39
|
+
};
|
|
40
|
+
import { randomUUID } from 'node:crypto';
|
|
41
|
+
import z from '@deepseek-ai/schemastery';
|
|
42
|
+
import { z as zod } from 'zod';
|
|
43
|
+
import { agentEvents } from '@stackstackstack/dsh-agent';
|
|
44
|
+
import { TypertRemoteService, Remote } from '@stackstackstack/dsh-typert-protocol';
|
|
45
|
+
import { applyGoalEvent, decodeGoalChange, emptyGoalFoldState, goalChangeRef, } from "./fold.js";
|
|
46
|
+
import { GOAL_CHANGE_VERSION, GoalError, GoalId, } from "./runtime.js";
|
|
47
|
+
export { GOAL_CHANGE_VERSION, GoalError, GoalId } from "./runtime.js";
|
|
48
|
+
export { decodeGoalChange, foldGoal, goalChangeRef } from "./fold.js";
|
|
49
|
+
/** Wire payload schema of the `goal` projection (whole current goal or pre-create/cleared null). */
|
|
50
|
+
const goalProjectionSchema = zod.union([
|
|
51
|
+
zod.object({
|
|
52
|
+
goal: zod.object({
|
|
53
|
+
id: zod.string().min(1),
|
|
54
|
+
revision: zod.number().int().positive(),
|
|
55
|
+
objective: zod.string().min(1),
|
|
56
|
+
phase: zod.union([zod.literal('active'), zod.literal('paused'), zod.literal('blocked'), zod.literal('complete')]),
|
|
57
|
+
blockedReason: zod.object({ code: zod.string(), message: zod.string() }).optional(),
|
|
58
|
+
maxGoalRounds: zod.number().int().positive(),
|
|
59
|
+
}),
|
|
60
|
+
roundsStarted: zod.number().int().nonnegative(),
|
|
61
|
+
createdAt: zod.number(),
|
|
62
|
+
updatedAt: zod.number(),
|
|
63
|
+
}),
|
|
64
|
+
zod.null(),
|
|
65
|
+
]);
|
|
66
|
+
/**
|
|
67
|
+
* Light last-wins fold of the `goal` projection unit. Unlike the strict
|
|
68
|
+
* replay fold (fold.ts: transition validation, fail-loud on malformed
|
|
69
|
+
* changes, Set-typed state), this transition is projection-grade: the state
|
|
70
|
+
* is plain JSON (persisted-cache precondition), any non-goal or malformed
|
|
71
|
+
* event returns the same reference (the registry's Object.is gate — the
|
|
72
|
+
* title/todos posture), and correctness of the written change is the write
|
|
73
|
+
* side's job (GoalService validated it before appending; the package
|
|
74
|
+
* invariant rejects a violating stream fail-loud where it is installed).
|
|
75
|
+
* @param state - the projection covering all prior events.
|
|
76
|
+
* @param event - the next committed session event.
|
|
77
|
+
* @returns the next projection (same reference when the event is not a goal change).
|
|
78
|
+
*/
|
|
79
|
+
export function applyGoalProjection(state, event) {
|
|
80
|
+
if (event.type !== 'goal/change')
|
|
81
|
+
return state;
|
|
82
|
+
let change;
|
|
83
|
+
try {
|
|
84
|
+
change = decodeGoalChange(event.data);
|
|
85
|
+
}
|
|
86
|
+
catch (_invalidPersistedGoalChange) {
|
|
87
|
+
return state;
|
|
88
|
+
}
|
|
89
|
+
if (change === undefined)
|
|
90
|
+
return state;
|
|
91
|
+
return change.operation === 'clear'
|
|
92
|
+
? null
|
|
93
|
+
: {
|
|
94
|
+
goal: change.goal,
|
|
95
|
+
roundsStarted: change.roundsStarted,
|
|
96
|
+
createdAt: change.createdAt,
|
|
97
|
+
updatedAt: change.updatedAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Validate a caller-visible positive safe-integer round cap. */
|
|
101
|
+
function resolveMaxGoalRounds(value) {
|
|
102
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
103
|
+
throw new GoalError('maxGoalRounds must be a positive safe integer', 'GOAL_INVALID_MAX_ROUNDS');
|
|
104
|
+
}
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
/** Validate and normalize an objective at the domain boundary. */
|
|
108
|
+
function resolveObjective(value) {
|
|
109
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
110
|
+
throw new GoalError('goal objective must be a non-empty string', 'GOAL_INVALID_OBJECTIVE');
|
|
111
|
+
}
|
|
112
|
+
return value.trim();
|
|
113
|
+
}
|
|
114
|
+
/** Materialize deployment defaults and validate one create request. */
|
|
115
|
+
function resolveCreateGoal(request, defaultMaxGoalRounds) {
|
|
116
|
+
return {
|
|
117
|
+
objective: resolveObjective(request.objective),
|
|
118
|
+
maxGoalRounds: resolveMaxGoalRounds(request.maxGoalRounds ?? defaultMaxGoalRounds),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Validate and detach one policy-owned blocker explanation. */
|
|
122
|
+
function resolveBlockReason(reason) {
|
|
123
|
+
const record = typeof reason === 'object' && reason !== null && !Array.isArray(reason)
|
|
124
|
+
? reason
|
|
125
|
+
: undefined;
|
|
126
|
+
const code = record?.['code'];
|
|
127
|
+
const message = record?.['message'];
|
|
128
|
+
if (typeof code !== 'string' || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(code)
|
|
129
|
+
|| typeof message !== 'string' || message.trim().length === 0) {
|
|
130
|
+
throw new GoalError('goal block reason requires a lower-kebab-case code and a non-empty message', 'GOAL_INVALID_BLOCK_REASON');
|
|
131
|
+
}
|
|
132
|
+
return { code, message: message.trim() };
|
|
133
|
+
}
|
|
134
|
+
/** Goal service (`ctx.goals`) backed exclusively by the owning session log. */
|
|
135
|
+
let GoalService = (() => {
|
|
136
|
+
let _classSuper = TypertRemoteService;
|
|
137
|
+
let _instanceExtraInitializers = [];
|
|
138
|
+
let _edit_decorators;
|
|
139
|
+
let _pause_decorators;
|
|
140
|
+
let _resume_decorators;
|
|
141
|
+
let _complete_decorators;
|
|
142
|
+
let _clear_decorators;
|
|
143
|
+
let _remoteExportCreate_decorators;
|
|
144
|
+
return class GoalService extends _classSuper {
|
|
145
|
+
static {
|
|
146
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
147
|
+
_edit_decorators = [Remote('edit')];
|
|
148
|
+
_pause_decorators = [Remote('pause')];
|
|
149
|
+
_resume_decorators = [Remote('resume')];
|
|
150
|
+
_complete_decorators = [Remote('complete')];
|
|
151
|
+
_clear_decorators = [Remote('clear')];
|
|
152
|
+
_remoteExportCreate_decorators = [Remote('create')];
|
|
153
|
+
__esDecorate(this, null, _edit_decorators, { kind: "method", name: "edit", static: false, private: false, access: { has: obj => "edit" in obj, get: obj => obj.edit }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
154
|
+
__esDecorate(this, null, _pause_decorators, { kind: "method", name: "pause", static: false, private: false, access: { has: obj => "pause" in obj, get: obj => obj.pause }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
155
|
+
__esDecorate(this, null, _resume_decorators, { kind: "method", name: "resume", static: false, private: false, access: { has: obj => "resume" in obj, get: obj => obj.resume }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
156
|
+
__esDecorate(this, null, _complete_decorators, { kind: "method", name: "complete", static: false, private: false, access: { has: obj => "complete" in obj, get: obj => obj.complete }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
157
|
+
__esDecorate(this, null, _clear_decorators, { kind: "method", name: "clear", static: false, private: false, access: { has: obj => "clear" in obj, get: obj => obj.clear }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
158
|
+
__esDecorate(this, null, _remoteExportCreate_decorators, { kind: "method", name: "remoteExportCreate", static: false, private: false, access: { has: obj => "remoteExportCreate" in obj, get: obj => obj.remoteExportCreate }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
159
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
160
|
+
}
|
|
161
|
+
static inject = ['agents'];
|
|
162
|
+
static Config = z.object({
|
|
163
|
+
defaultMaxGoalRounds: z.number().default(256),
|
|
164
|
+
});
|
|
165
|
+
resolved = __runInitializers(this, _instanceExtraInitializers);
|
|
166
|
+
caches = new WeakMap();
|
|
167
|
+
constructor(ctx, config = {}) {
|
|
168
|
+
super(ctx, 'goals');
|
|
169
|
+
this.resolved = {
|
|
170
|
+
defaultMaxGoalRounds: resolveMaxGoalRounds(config.defaultMaxGoalRounds ?? 256),
|
|
171
|
+
};
|
|
172
|
+
ctx.on('agent/session-start', ({ agent }) => {
|
|
173
|
+
this.cache(agent.session).activation = 'disarmed';
|
|
174
|
+
});
|
|
175
|
+
// The `goal` projection unit: last-wins fold of goal/change whole values
|
|
176
|
+
// (see applyGoalProjection). The unit child activates only when a
|
|
177
|
+
// projection registry is composed (headless assemblies stay unaffected).
|
|
178
|
+
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
179
|
+
projectionCtx.sessionProjections.register({
|
|
180
|
+
key: 'goal',
|
|
181
|
+
schema: goalProjectionSchema,
|
|
182
|
+
init: () => null,
|
|
183
|
+
apply: applyGoalProjection,
|
|
184
|
+
view: state => state,
|
|
185
|
+
stateVersion: 4,
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Read the current goal for one exact live agent.
|
|
191
|
+
* @param agent - owning live agent.
|
|
192
|
+
* @returns a fresh view or `undefined` when no goal is current.
|
|
193
|
+
* @throws {@link GoalError} when the agent is not the registry's live instance.
|
|
194
|
+
*/
|
|
195
|
+
get(agent) {
|
|
196
|
+
this.assertLive(agent);
|
|
197
|
+
const cache = this.cache(agent.session);
|
|
198
|
+
this.sync(agent.session, cache);
|
|
199
|
+
return this.view(cache);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Remove process-local continuation authority without changing durable goal
|
|
203
|
+
* phase or revision. Lifecycle owners use this before unloading a driver;
|
|
204
|
+
* a later human-authorized {@link resume} records the new activation edge.
|
|
205
|
+
* @param agent - owning live agent.
|
|
206
|
+
* @returns a fresh disarmed view, or `undefined` when no goal is current.
|
|
207
|
+
*/
|
|
208
|
+
disarm(agent) {
|
|
209
|
+
this.assertLive(agent);
|
|
210
|
+
const cache = this.cache(agent.session);
|
|
211
|
+
this.sync(agent.session, cache);
|
|
212
|
+
cache.activation = 'disarmed';
|
|
213
|
+
return this.view(cache);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Create and arm a goal. A completed goal may be replaced; every other
|
|
217
|
+
* current phase must be cleared or resumed instead.
|
|
218
|
+
* @param agent - owning live agent.
|
|
219
|
+
* @param request - objective and optional round cap.
|
|
220
|
+
* @returns the created live view.
|
|
221
|
+
*/
|
|
222
|
+
create(agent, request) {
|
|
223
|
+
const spec = resolveCreateGoal(request, this.resolved.defaultMaxGoalRounds);
|
|
224
|
+
const cache = this.prepareMutation(agent);
|
|
225
|
+
const current = cache.state.goal;
|
|
226
|
+
if (current !== undefined && current.phase !== 'complete') {
|
|
227
|
+
throw new GoalError(`goal "${current.id}" already exists with phase "${current.phase}"`, 'GOAL_ALREADY_EXISTS');
|
|
228
|
+
}
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
const goal = {
|
|
231
|
+
id: GoalId(`goal-${randomUUID()}`),
|
|
232
|
+
revision: 1,
|
|
233
|
+
objective: spec.objective,
|
|
234
|
+
phase: 'active',
|
|
235
|
+
maxGoalRounds: spec.maxGoalRounds,
|
|
236
|
+
};
|
|
237
|
+
return this.commitSnapshot(agent, cache, 'create', goal, 0, now, now, 'armed');
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Edit objective and/or round cap without changing phase.
|
|
241
|
+
* @param agent - owning live agent.
|
|
242
|
+
* @param ref - expected current revision.
|
|
243
|
+
* @param request - at least one replacement field.
|
|
244
|
+
* @returns the edited view.
|
|
245
|
+
*/
|
|
246
|
+
edit(agent, ref, request) {
|
|
247
|
+
const cache = this.prepareMutation(agent);
|
|
248
|
+
const current = this.expectCurrent(cache, ref);
|
|
249
|
+
if (request.objective === undefined && request.maxGoalRounds === undefined) {
|
|
250
|
+
throw new GoalError('goal edit requires objective and/or maxGoalRounds', 'GOAL_INVALID_EDIT');
|
|
251
|
+
}
|
|
252
|
+
const goal = {
|
|
253
|
+
...current,
|
|
254
|
+
revision: current.revision + 1,
|
|
255
|
+
...request.objective === undefined ? {} : { objective: resolveObjective(request.objective) },
|
|
256
|
+
...request.maxGoalRounds === undefined ? {} : { maxGoalRounds: resolveMaxGoalRounds(request.maxGoalRounds) },
|
|
257
|
+
};
|
|
258
|
+
return this.commitCurrent(agent, cache, 'edit', goal, cache.activation);
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Pause an active goal and disarm automatic continuation.
|
|
262
|
+
* @param agent - owning live agent.
|
|
263
|
+
* @param ref - expected current revision.
|
|
264
|
+
* @returns the paused view.
|
|
265
|
+
*/
|
|
266
|
+
pause(agent, ref) {
|
|
267
|
+
return this.transition(agent, ref, 'pause', ['active'], 'paused', 'disarmed');
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Resume and arm a stopped goal, or rearm an active goal after a
|
|
271
|
+
* session-start edge, while its round budget still has capacity.
|
|
272
|
+
* @param agent - owning live agent.
|
|
273
|
+
* @param ref - expected current revision.
|
|
274
|
+
* @returns the active view.
|
|
275
|
+
*/
|
|
276
|
+
resume(agent, ref) {
|
|
277
|
+
const cache = this.prepareMutation(agent);
|
|
278
|
+
const current = this.expectCurrent(cache, ref);
|
|
279
|
+
const resumable = ['active', 'paused', 'blocked'];
|
|
280
|
+
if (!resumable.includes(current.phase)) {
|
|
281
|
+
throw this.transitionError(current, 'resume', resumable);
|
|
282
|
+
}
|
|
283
|
+
if (current.phase === 'active' && cache.activation === 'armed') {
|
|
284
|
+
throw new GoalError(`goal "${current.id}" is already active and armed`, 'GOAL_INVALID_TRANSITION');
|
|
285
|
+
}
|
|
286
|
+
if (cache.state.roundsStarted >= current.maxGoalRounds) {
|
|
287
|
+
throw new GoalError(`goal "${current.id}" exhausted ${current.maxGoalRounds} goal rounds; increase maxGoalRounds before resuming`, 'GOAL_INVALID_TRANSITION');
|
|
288
|
+
}
|
|
289
|
+
return this.commitCurrent(agent, cache, 'resume', this.withPhase(current, 'active'), 'armed');
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Mark a current non-complete goal complete and disarm it.
|
|
293
|
+
* @param agent - owning live agent.
|
|
294
|
+
* @param ref - expected current revision.
|
|
295
|
+
* @returns the completed view.
|
|
296
|
+
*/
|
|
297
|
+
complete(agent, ref) {
|
|
298
|
+
return this.transition(agent, ref, 'complete', ['active', 'paused', 'blocked'], 'complete', 'disarmed');
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Mark an active goal blocked and disarm it.
|
|
302
|
+
* @param agent - owning live agent.
|
|
303
|
+
* @param ref - expected current revision.
|
|
304
|
+
* @param reason - policy-owned stable code and human-readable explanation.
|
|
305
|
+
* @returns the blocked view with its durable reason.
|
|
306
|
+
*/
|
|
307
|
+
block(agent, ref, reason) {
|
|
308
|
+
const cache = this.prepareMutation(agent);
|
|
309
|
+
const current = this.expectCurrent(cache, ref);
|
|
310
|
+
if (current.phase !== 'active') {
|
|
311
|
+
throw this.transitionError(current, 'block', ['active']);
|
|
312
|
+
}
|
|
313
|
+
return this.commitCurrent(agent, cache, 'block', { ...this.withPhase(current, 'blocked'), blockedReason: resolveBlockReason(reason) }, 'disarmed');
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Clear the current goal while retaining a durable tombstone and history.
|
|
317
|
+
* @param agent - owning live agent.
|
|
318
|
+
* @param ref - expected current revision.
|
|
319
|
+
* @returns the tombstone ref whose revision is one past the cleared snapshot.
|
|
320
|
+
*/
|
|
321
|
+
clear(agent, ref) {
|
|
322
|
+
const cache = this.prepareMutation(agent);
|
|
323
|
+
const current = this.expectCurrent(cache, ref);
|
|
324
|
+
const tombstone = { id: current.id, revision: current.revision + 1 };
|
|
325
|
+
const change = {
|
|
326
|
+
kind: 'goal/change',
|
|
327
|
+
version: GOAL_CHANGE_VERSION,
|
|
328
|
+
operation: 'clear',
|
|
329
|
+
cleared: tombstone,
|
|
330
|
+
clearedAt: this.nextMutationTime(cache),
|
|
331
|
+
};
|
|
332
|
+
this.commit(agent, cache, change, 'disarmed');
|
|
333
|
+
return { ...tombstone };
|
|
334
|
+
}
|
|
335
|
+
/** Resolve and validate the cache used by a mutation. */
|
|
336
|
+
prepareMutation(agent) {
|
|
337
|
+
this.assertLive(agent);
|
|
338
|
+
const cache = this.cache(agent.session);
|
|
339
|
+
this.sync(agent.session, cache);
|
|
340
|
+
return cache;
|
|
341
|
+
}
|
|
342
|
+
/** Reject stale or missing current-state refs. */
|
|
343
|
+
expectCurrent(cache, ref) {
|
|
344
|
+
const current = cache.state.goal;
|
|
345
|
+
if (current === undefined)
|
|
346
|
+
throw new GoalError('no current goal', 'GOAL_NOT_FOUND');
|
|
347
|
+
if (ref.id !== current.id || ref.revision !== current.revision) {
|
|
348
|
+
throw new GoalError(`stale goal ref "${ref.id}" revision ${ref.revision}; current is "${current.id}" revision ${current.revision}`, 'GOAL_STALE_REVISION');
|
|
349
|
+
}
|
|
350
|
+
return current;
|
|
351
|
+
}
|
|
352
|
+
/** Enforce exact live-agent identity rather than trusting a matching id. */
|
|
353
|
+
assertLive(agent) {
|
|
354
|
+
if (this.ctx.agents.get(agent.id) !== agent) {
|
|
355
|
+
throw new GoalError(`agent "${agent.id}" is not live in this registry`, 'GOAL_AGENT_NOT_LIVE');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/** Return the per-session cache, folding a seed once with activation disarmed. */
|
|
359
|
+
cache(session) {
|
|
360
|
+
let cache = this.caches.get(session);
|
|
361
|
+
if (cache !== undefined)
|
|
362
|
+
return cache;
|
|
363
|
+
const state = emptyGoalFoldState();
|
|
364
|
+
for (const event of session.events)
|
|
365
|
+
applyGoalEvent(state, event);
|
|
366
|
+
cache = {
|
|
367
|
+
state,
|
|
368
|
+
activation: 'disarmed',
|
|
369
|
+
observedSeq: session.seq,
|
|
370
|
+
pendingActivation: undefined,
|
|
371
|
+
};
|
|
372
|
+
this.caches.set(session, cache);
|
|
373
|
+
return cache;
|
|
374
|
+
}
|
|
375
|
+
/** Incrementally observe durable events and reconcile local activation intent. */
|
|
376
|
+
sync(session, cache) {
|
|
377
|
+
for (const event of session.events.slice(cache.observedSeq)) {
|
|
378
|
+
applyGoalEvent(cache.state, event);
|
|
379
|
+
if (event.type === 'goal/change') {
|
|
380
|
+
cache.activation = cache.pendingActivation?.seq === event.seq
|
|
381
|
+
? cache.pendingActivation.activation
|
|
382
|
+
: 'disarmed';
|
|
383
|
+
}
|
|
384
|
+
cache.observedSeq += 1;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/** Build a new revision with one replacement phase. */
|
|
388
|
+
withPhase(current, phase) {
|
|
389
|
+
return {
|
|
390
|
+
id: current.id,
|
|
391
|
+
revision: current.revision + 1,
|
|
392
|
+
objective: current.objective,
|
|
393
|
+
phase,
|
|
394
|
+
maxGoalRounds: current.maxGoalRounds,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** Shared validated phase transition. */
|
|
398
|
+
transition(agent, ref, operation, allowed, phase, activation) {
|
|
399
|
+
const cache = this.prepareMutation(agent);
|
|
400
|
+
const current = this.expectCurrent(cache, ref);
|
|
401
|
+
if (!allowed.includes(current.phase))
|
|
402
|
+
throw this.transitionError(current, operation, allowed);
|
|
403
|
+
return this.commitCurrent(agent, cache, operation, this.withPhase(current, phase), activation);
|
|
404
|
+
}
|
|
405
|
+
/** Render a stable invalid-transition error. */
|
|
406
|
+
transitionError(current, operation, allowed) {
|
|
407
|
+
return new GoalError(`cannot ${operation} goal "${current.id}" from phase "${current.phase}"; expected ${allowed.join(' or ')}`, 'GOAL_INVALID_TRANSITION');
|
|
408
|
+
}
|
|
409
|
+
/** Commit a mutation that retains the current goal's derived counters/times. */
|
|
410
|
+
commitCurrent(agent, cache, operation, goal, activation) {
|
|
411
|
+
const createdAt = cache.state.createdAt;
|
|
412
|
+
/* v8 ignore next -- strict replay and every snapshot commit set createdAt whenever a current goal exists */
|
|
413
|
+
if (createdAt === undefined)
|
|
414
|
+
throw new Error('current goal cache lacks createdAt');
|
|
415
|
+
return this.commitSnapshot(agent, cache, operation, goal, cache.state.roundsStarted, createdAt, this.nextMutationTime(cache), activation);
|
|
416
|
+
}
|
|
417
|
+
/** Clamp a current goal's next timestamp across backward wall-clock movement. */
|
|
418
|
+
nextMutationTime(cache) {
|
|
419
|
+
const updatedAt = cache.state.updatedAt;
|
|
420
|
+
/* v8 ignore next -- strict replay and every snapshot commit set updatedAt whenever a current goal exists */
|
|
421
|
+
if (updatedAt === undefined)
|
|
422
|
+
throw new Error('current goal cache lacks updatedAt');
|
|
423
|
+
return Math.max(Date.now(), updatedAt);
|
|
424
|
+
}
|
|
425
|
+
/** Build and commit one full-snapshot mutation. */
|
|
426
|
+
commitSnapshot(agent, cache, operation, goal, roundsStarted, createdAt, updatedAt, activation) {
|
|
427
|
+
const change = {
|
|
428
|
+
kind: 'goal/change',
|
|
429
|
+
version: GOAL_CHANGE_VERSION,
|
|
430
|
+
operation,
|
|
431
|
+
goal,
|
|
432
|
+
roundsStarted,
|
|
433
|
+
createdAt,
|
|
434
|
+
updatedAt,
|
|
435
|
+
};
|
|
436
|
+
this.commit(agent, cache, change, activation);
|
|
437
|
+
const view = this.view(cache);
|
|
438
|
+
/* v8 ignore next -- the durable goal event installs the snapshot before this read */
|
|
439
|
+
if (view === undefined)
|
|
440
|
+
throw new Error('snapshot commit cleared the goal unexpectedly');
|
|
441
|
+
return view;
|
|
442
|
+
}
|
|
443
|
+
/** Commit one mutation into the goal log, cache, and live event stream. */
|
|
444
|
+
commit(agent, cache, change, activation) {
|
|
445
|
+
const ref = goalChangeRef(change);
|
|
446
|
+
cache.pendingActivation = { seq: agent.session.seq, activation };
|
|
447
|
+
try {
|
|
448
|
+
agent.session.append('goal/change', change);
|
|
449
|
+
this.sync(agent.session, cache);
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
cache.pendingActivation = undefined;
|
|
453
|
+
}
|
|
454
|
+
const goal = this.view(cache);
|
|
455
|
+
const notification = {
|
|
456
|
+
operation: change.operation,
|
|
457
|
+
ref: { ...ref },
|
|
458
|
+
...goal === undefined ? {} : { goal },
|
|
459
|
+
};
|
|
460
|
+
agentEvents(this.ctx, agent).emit('goal/changed', { change: notification });
|
|
461
|
+
}
|
|
462
|
+
/** Build a detached current view. */
|
|
463
|
+
view(cache) {
|
|
464
|
+
const goal = cache.state.goal;
|
|
465
|
+
const createdAt = cache.state.createdAt;
|
|
466
|
+
const updatedAt = cache.state.updatedAt;
|
|
467
|
+
if (goal === undefined)
|
|
468
|
+
return undefined;
|
|
469
|
+
/* v8 ignore next 3 -- strict replay and snapshot commits establish both timestamps with every current goal */
|
|
470
|
+
if (createdAt === undefined || updatedAt === undefined) {
|
|
471
|
+
throw new Error(`goal "${goal.id}" cache lacks timestamps`);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
...goal,
|
|
475
|
+
roundsStarted: cache.state.roundsStarted,
|
|
476
|
+
createdAt,
|
|
477
|
+
updatedAt,
|
|
478
|
+
activation: cache.activation,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Create one Goal through the remote boundary.
|
|
483
|
+
* @param agent - exact live Agent resolved from the wire identity.
|
|
484
|
+
* @param request - objective and optional round cap.
|
|
485
|
+
* @returns the created Goal identity.
|
|
486
|
+
*/
|
|
487
|
+
remoteExportCreate(agent, request) {
|
|
488
|
+
const view = this.create(agent, request);
|
|
489
|
+
return { ref: { id: view.id, revision: view.revision } };
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
})();
|
|
493
|
+
export { GoalService };
|
|
494
|
+
export default GoalService;
|
|
495
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned durable goal-stream invariants. @module @stackstackstack/dsh-goal/invariant */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "goal-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the goal-stream invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Package-owned durable goal-stream invariants. @module @stackstackstack/dsh-goal/invariant */
|
|
2
|
+
import { applyGoalEvent, emptyGoalFoldState } from "./fold.js";
|
|
3
|
+
const PACKAGE_NAME = '@stackstackstack/dsh-goal';
|
|
4
|
+
/** Cordis companion plugin name. */
|
|
5
|
+
export const name = 'goal-invariant';
|
|
6
|
+
/** Service required before the companion can reserve package ownership. */
|
|
7
|
+
export const inject = ['invariants'];
|
|
8
|
+
/** Copy the independent fold before validating one candidate event. */
|
|
9
|
+
function cloneState(state) {
|
|
10
|
+
return {
|
|
11
|
+
goal: state.goal,
|
|
12
|
+
roundsStarted: state.roundsStarted,
|
|
13
|
+
createdAt: state.createdAt,
|
|
14
|
+
updatedAt: state.updatedAt,
|
|
15
|
+
lastRef: state.lastRef,
|
|
16
|
+
seenGoalIds: new Set(state.seenGoalIds),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Apply one event through the strict goal decoder and attribute failures. */
|
|
20
|
+
function applyChecked(state, event, fail) {
|
|
21
|
+
try {
|
|
22
|
+
applyGoalEvent(state, event);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
/* v8 ignore next -- the strict goal decoder throws Error instances */
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
fail(`session event ${event.seq} violates the durable goal stream: ${message}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** Install an independent incremental fold over every attached session. */
|
|
31
|
+
const install = Object.assign((ctx, fail) => {
|
|
32
|
+
const states = new WeakMap();
|
|
33
|
+
const staged = new WeakMap();
|
|
34
|
+
const seed = (session) => {
|
|
35
|
+
const state = emptyGoalFoldState();
|
|
36
|
+
for (const event of session.events)
|
|
37
|
+
applyChecked(state, event, fail);
|
|
38
|
+
states.set(session, state);
|
|
39
|
+
return state;
|
|
40
|
+
};
|
|
41
|
+
/* v8 ignore next -- session/event always follows list() or session/created seeding */
|
|
42
|
+
const stateFor = (session) => states.get(session) ?? seed(session);
|
|
43
|
+
for (const session of ctx.sessions.list())
|
|
44
|
+
seed(session);
|
|
45
|
+
ctx.on('session/created', (session) => { seed(session); }, { global: true });
|
|
46
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
47
|
+
if (eventName !== 'session/event')
|
|
48
|
+
return;
|
|
49
|
+
const [session, event] = args;
|
|
50
|
+
const state = cloneState(stateFor(session));
|
|
51
|
+
applyChecked(state, event, fail);
|
|
52
|
+
staged.set(event, { session, state });
|
|
53
|
+
}, { global: true });
|
|
54
|
+
ctx.on('session/event', (session, event) => {
|
|
55
|
+
const candidate = staged.get(event);
|
|
56
|
+
/* v8 ignore next 2 -- internal/dispatch stages the exact callback arguments */
|
|
57
|
+
if (candidate === undefined || candidate.session !== session) {
|
|
58
|
+
return fail('session/event reached publication without matching goal-fold validation');
|
|
59
|
+
}
|
|
60
|
+
staged.delete(event);
|
|
61
|
+
states.set(session, candidate.state);
|
|
62
|
+
}, { global: true });
|
|
63
|
+
}, { inject: ['sessions'] });
|
|
64
|
+
/**
|
|
65
|
+
* Register the goal-stream invariant companion.
|
|
66
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
67
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
68
|
+
*/
|
|
69
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
70
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Runtime constructors and protocol constants for the goal domain. */
|
|
2
|
+
import { HarnessError } from '@stackstackstack/dsh-llm';
|
|
3
|
+
import type { GoalId as GoalIdType } from './types.ts';
|
|
4
|
+
import type { GoalErrorCode } from './domain.ts';
|
|
5
|
+
/** Version of the goal change embedded in a round-zero message source. */
|
|
6
|
+
export declare const GOAL_CHANGE_VERSION = 1;
|
|
7
|
+
/**
|
|
8
|
+
* Brand a string as a goal id.
|
|
9
|
+
* @param id - raw goal identifier.
|
|
10
|
+
* @returns the same string with the compile-time brand.
|
|
11
|
+
*/
|
|
12
|
+
export declare function GoalId(id: string): GoalIdType;
|
|
13
|
+
/** Error returned by the goal domain boundary. */
|
|
14
|
+
export declare class GoalError extends HarnessError {
|
|
15
|
+
/**
|
|
16
|
+
* @param message - human-readable rejection reason.
|
|
17
|
+
* @param code - stable machine-routable classification.
|
|
18
|
+
*/
|
|
19
|
+
constructor(message: string, code: GoalErrorCode);
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Runtime constructors and protocol constants for the goal domain. */
|
|
2
|
+
import { HarnessError } from '@stackstackstack/dsh-llm';
|
|
3
|
+
/** Version of the goal change embedded in a round-zero message source. */
|
|
4
|
+
export const GOAL_CHANGE_VERSION = 1;
|
|
5
|
+
/**
|
|
6
|
+
* Brand a string as a goal id.
|
|
7
|
+
* @param id - raw goal identifier.
|
|
8
|
+
* @returns the same string with the compile-time brand.
|
|
9
|
+
*/
|
|
10
|
+
export function GoalId(id) {
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
/** Error returned by the goal domain boundary. */
|
|
14
|
+
export class GoalError extends HarnessError {
|
|
15
|
+
/**
|
|
16
|
+
* @param message - human-readable rejection reason.
|
|
17
|
+
* @param code - stable machine-routable classification.
|
|
18
|
+
*/
|
|
19
|
+
// Keep the constructor to narrow HarnessError's string code at this boundary.
|
|
20
|
+
// oxlint-disable-next-line typescript/no-useless-constructor -- type-only narrowing
|
|
21
|
+
constructor(message, code) {
|
|
22
|
+
super(message, code);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=runtime.js.map
|