@noetic-tools/core 4.0.0 → 5.0.0
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.
|
@@ -136,6 +136,8 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
136
136
|
readonly _contextLayers?: ContextLayer[];
|
|
137
137
|
private readonly client?;
|
|
138
138
|
private readonly channelStore;
|
|
139
|
+
/** Re-entrant `run()` depth per root context id — see `executeClosingChannels`. */
|
|
140
|
+
private readonly rootRunDepth;
|
|
139
141
|
private readonly callModelOverride?;
|
|
140
142
|
private readonly defaultDeliveryMode;
|
|
141
143
|
private readonly streamIdleTimeoutMs;
|
|
@@ -195,6 +197,15 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
195
197
|
/** Run all layer `init` hooks before the first step executes. Per spec 11,
|
|
196
198
|
* init MUST complete before any recall fires so layer state is populated. */
|
|
197
199
|
private initAndRun;
|
|
200
|
+
/**
|
|
201
|
+
* Run `execute`, closing the execution's external channels when the
|
|
202
|
+
* OUTERMOST run on a root context finishes (spec 06 Lifecycle: handles flip
|
|
203
|
+
* `closed`, channel streams end). Patterns re-enter `run()` with the same
|
|
204
|
+
* root context (`ctx.harness.run(step, input, ctx)`), so a depth count —
|
|
205
|
+
* not the return itself — marks root completion. Child-context runs never
|
|
206
|
+
* close anything.
|
|
207
|
+
*/
|
|
208
|
+
private executeClosingChannels;
|
|
198
209
|
/**
|
|
199
210
|
* Idempotently run layer `init` hooks for `ctx` (keyed by `ctx.id`). Init
|
|
200
211
|
* MUST complete before any recall/store fires so init-bearing layers are not
|
|
@@ -227,6 +238,7 @@ export declare class AgentHarness<TParams extends Record<string, unknown> = Reco
|
|
|
227
238
|
}): Promise<T>;
|
|
228
239
|
tryRecv<T>(channel: Channel<T>, _ctx: Context): T | null;
|
|
229
240
|
getChannelHandle<T>(channel: ExternalChannel<T>, executionId: string): ChannelHandle<T>;
|
|
241
|
+
getChannelStream<T>(channel: ExternalChannel<T>, executionId: string): AsyncIterable<T>;
|
|
230
242
|
/** Resolves layer-provided tools and merges with step tools into ctx.unifiedTools. */
|
|
231
243
|
private setUnifiedTools;
|
|
232
244
|
private toExecCtx;
|
|
@@ -146,6 +146,8 @@ export class AgentHarness {
|
|
|
146
146
|
_contextLayers;
|
|
147
147
|
client;
|
|
148
148
|
channelStore;
|
|
149
|
+
/** Re-entrant `run()` depth per root context id — see `executeClosingChannels`. */
|
|
150
|
+
rootRunDepth = new Map();
|
|
149
151
|
callModelOverride;
|
|
150
152
|
defaultDeliveryMode;
|
|
151
153
|
streamIdleTimeoutMs;
|
|
@@ -424,13 +426,46 @@ export class AgentHarness {
|
|
|
424
426
|
// configuring `context` + `storage` must "just work" without going through
|
|
425
427
|
// `execute()`/`seedSessionHistory`. Idempotent — see `ensureLayersInit`.
|
|
426
428
|
await this.ensureLayersInit(ctx);
|
|
427
|
-
return
|
|
429
|
+
return this.executeClosingChannels(s, input, ctx);
|
|
428
430
|
}
|
|
429
431
|
/** Run all layer `init` hooks before the first step executes. Per spec 11,
|
|
430
432
|
* init MUST complete before any recall fires so layer state is populated. */
|
|
431
433
|
async initAndRun(s, input, ctx) {
|
|
432
434
|
await this.ensureLayersInit(ctx);
|
|
433
|
-
return
|
|
435
|
+
return this.executeClosingChannels(s, input, ctx);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Run `execute`, closing the execution's external channels when the
|
|
439
|
+
* OUTERMOST run on a root context finishes (spec 06 Lifecycle: handles flip
|
|
440
|
+
* `closed`, channel streams end). Patterns re-enter `run()` with the same
|
|
441
|
+
* root context (`ctx.harness.run(step, input, ctx)`), so a depth count —
|
|
442
|
+
* not the return itself — marks root completion. Child-context runs never
|
|
443
|
+
* close anything.
|
|
444
|
+
*/
|
|
445
|
+
async executeClosingChannels(s, input, ctx) {
|
|
446
|
+
if (ctx.parent !== null) {
|
|
447
|
+
return execute(s, input, ctx);
|
|
448
|
+
}
|
|
449
|
+
const depth = this.rootRunDepth.get(ctx.id) ?? 0;
|
|
450
|
+
if (depth === 0) {
|
|
451
|
+
// A restored or sequentially re-run root context reuses its id — clear
|
|
452
|
+
// any closure from a previous completion so its channels work again.
|
|
453
|
+
this.channelStore.openExecution(ctx.id);
|
|
454
|
+
}
|
|
455
|
+
this.rootRunDepth.set(ctx.id, depth + 1);
|
|
456
|
+
try {
|
|
457
|
+
return await execute(s, input, ctx);
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
const remaining = (this.rootRunDepth.get(ctx.id) ?? 1) - 1;
|
|
461
|
+
if (remaining > 0) {
|
|
462
|
+
this.rootRunDepth.set(ctx.id, remaining);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
this.rootRunDepth.delete(ctx.id);
|
|
466
|
+
this.channelStore.closeExecution(ctx.id);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
434
469
|
}
|
|
435
470
|
/**
|
|
436
471
|
* Idempotently run layer `init` hooks for `ctx` (keyed by `ctx.id`). Init
|
|
@@ -512,6 +547,9 @@ export class AgentHarness {
|
|
|
512
547
|
getChannelHandle(channel, executionId) {
|
|
513
548
|
return this.channelStore.getHandle(channel, executionId);
|
|
514
549
|
}
|
|
550
|
+
getChannelStream(channel, executionId) {
|
|
551
|
+
return this.channelStore.subscribe(channel, executionId);
|
|
552
|
+
}
|
|
515
553
|
/** Resolves layer-provided tools and merges with step tools into ctx.unifiedTools. */
|
|
516
554
|
setUnifiedTools(ctx, stepTools) {
|
|
517
555
|
const layers = ctx.layers;
|
|
@@ -744,6 +782,11 @@ export class AgentHarness {
|
|
|
744
782
|
for (const node of tree.reverse()) {
|
|
745
783
|
await this.teardownCancelledContext(node);
|
|
746
784
|
}
|
|
785
|
+
// Cancellation completes the root execution (spec 06 Lifecycle): close
|
|
786
|
+
// its external channels so handles flip `closed` and streams end.
|
|
787
|
+
if (ctx.parent === null) {
|
|
788
|
+
this.channelStore.closeExecution(ctx.id);
|
|
789
|
+
}
|
|
747
790
|
}
|
|
748
791
|
/**
|
|
749
792
|
* Run the layer lifecycle's abort path for one cancelled context:
|
|
@@ -12,6 +12,15 @@ export interface ChannelSendOpts {
|
|
|
12
12
|
export declare class ChannelStore {
|
|
13
13
|
private channels;
|
|
14
14
|
private closedExecutions;
|
|
15
|
+
/**
|
|
16
|
+
* Ids a `ChannelHandle` was issued for. Handles have no dispose lifecycle,
|
|
17
|
+
* so these are the only ids whose closure must be durably recorded (for
|
|
18
|
+
* `handle.closed` / `channel_closed`). Streams are closed directly through
|
|
19
|
+
* `subscribersByExecution`, so stream-only ids never accumulate here — a
|
|
20
|
+
* session harness closing one root id per turn stays bounded.
|
|
21
|
+
*/
|
|
22
|
+
private handleObserved;
|
|
23
|
+
private subscribersByExecution;
|
|
15
24
|
private getOrCreate;
|
|
16
25
|
/**
|
|
17
26
|
* Subscribe to wake notifications on a channel without consuming any value.
|
|
@@ -53,6 +62,30 @@ export declare class ChannelStore {
|
|
|
53
62
|
* dropped (with a warning) so external callers never block.
|
|
54
63
|
*/
|
|
55
64
|
private sendExternal;
|
|
65
|
+
/**
|
|
66
|
+
* External-reader subscription (spec 06, External Subscriptions). Delivery
|
|
67
|
+
* by mode: `topic` is a persistent non-lossy tap (per-subscriber bounded
|
|
68
|
+
* buffer, no replay of pre-subscribe values), `queue` competes with internal
|
|
69
|
+
* `recv` waiters FIFO (external consumption frees parked senders), `value`
|
|
70
|
+
* yields the current value then conflated updates.
|
|
71
|
+
*
|
|
72
|
+
* Channel state is keyed by NAME — `executionId` bounds only the
|
|
73
|
+
* subscription's lifetime (it ends when `closeExecution(executionId)`
|
|
74
|
+
* runs; queued values present at close drain first), never which values
|
|
75
|
+
* are delivered. An id that is never closed yields an unbounded stream
|
|
76
|
+
* the caller ends with `iterator.return()`. External waiters never time
|
|
77
|
+
* out — they end via close instead. The returned iterable owns a single
|
|
78
|
+
* iterator (generator semantics): subscribe again for a second consumer.
|
|
79
|
+
*/
|
|
80
|
+
subscribe<T>(channel: ExternalChannel<T>, executionId: string): AsyncIterable<T>;
|
|
81
|
+
/** Shift the queue head and promote the oldest parked sender into the freed slot. */
|
|
82
|
+
private dequeueHead;
|
|
56
83
|
getHandle<T>(channel: ExternalChannel<T>, executionId: string): ChannelHandle<T>;
|
|
57
84
|
closeExecution(executionId: string): void;
|
|
85
|
+
/**
|
|
86
|
+
* A root run is (re)starting on this execution id: clear any previous
|
|
87
|
+
* closure so checkpoint-restored and sequentially re-run root contexts get
|
|
88
|
+
* working channels again instead of a permanently poisoned id.
|
|
89
|
+
*/
|
|
90
|
+
openExecution(executionId: string): void;
|
|
58
91
|
}
|
|
@@ -12,6 +12,15 @@ function cancelledError(signal) {
|
|
|
12
12
|
export class ChannelStore {
|
|
13
13
|
channels = new Map();
|
|
14
14
|
closedExecutions = new Set();
|
|
15
|
+
/**
|
|
16
|
+
* Ids a `ChannelHandle` was issued for. Handles have no dispose lifecycle,
|
|
17
|
+
* so these are the only ids whose closure must be durably recorded (for
|
|
18
|
+
* `handle.closed` / `channel_closed`). Streams are closed directly through
|
|
19
|
+
* `subscribersByExecution`, so stream-only ids never accumulate here — a
|
|
20
|
+
* session harness closing one root id per turn stays bounded.
|
|
21
|
+
*/
|
|
22
|
+
handleObserved = new Set();
|
|
23
|
+
subscribersByExecution = new Map();
|
|
15
24
|
getOrCreate(channel) {
|
|
16
25
|
let state = frameworkCast(this.channels.get(channel.name));
|
|
17
26
|
if (!state) {
|
|
@@ -25,6 +34,7 @@ export class ChannelStore {
|
|
|
25
34
|
pendingSenders: [],
|
|
26
35
|
topicSubscribers: new Set(),
|
|
27
36
|
wakeSubscribers: new Set(),
|
|
37
|
+
externalSubscribers: new Set(),
|
|
28
38
|
};
|
|
29
39
|
this.channels.set(channel.name, frameworkCast(state));
|
|
30
40
|
}
|
|
@@ -79,6 +89,9 @@ export class ChannelStore {
|
|
|
79
89
|
waiter.resolve(value);
|
|
80
90
|
}
|
|
81
91
|
}
|
|
92
|
+
for (const sub of state.externalSubscribers) {
|
|
93
|
+
sub.push(value);
|
|
94
|
+
}
|
|
82
95
|
break;
|
|
83
96
|
case 'queue':
|
|
84
97
|
if (state.queueWaiters.length > 0) {
|
|
@@ -96,6 +109,9 @@ export class ChannelStore {
|
|
|
96
109
|
for (const sub of state.topicSubscribers) {
|
|
97
110
|
sub(value);
|
|
98
111
|
}
|
|
112
|
+
for (const sub of state.externalSubscribers) {
|
|
113
|
+
sub.push(value);
|
|
114
|
+
}
|
|
99
115
|
break;
|
|
100
116
|
}
|
|
101
117
|
this.fireWakes(state);
|
|
@@ -206,10 +222,9 @@ export class ChannelStore {
|
|
|
206
222
|
}
|
|
207
223
|
return this.waitWithTimeout(state.valueWaiters, channel.name, timeout, signal);
|
|
208
224
|
case 'queue': {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return head;
|
|
225
|
+
const head = this.dequeueHead(state);
|
|
226
|
+
if (head) {
|
|
227
|
+
return head.value;
|
|
213
228
|
}
|
|
214
229
|
// capacity-0 edge: senders can be parked while the queue is empty —
|
|
215
230
|
// hand the oldest parked value straight to this receiver.
|
|
@@ -268,12 +283,10 @@ export class ChannelStore {
|
|
|
268
283
|
case 'value':
|
|
269
284
|
return state.hasValue ? state.currentValue : null;
|
|
270
285
|
case 'queue': {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
return null;
|
|
286
|
+
// Explicit head check (not `?? null`): a stored `undefined` must come
|
|
287
|
+
// back as `undefined`, distinguishable from the empty-queue sentinel.
|
|
288
|
+
const head = this.dequeueHead(state);
|
|
289
|
+
return head ? head.value : null;
|
|
277
290
|
}
|
|
278
291
|
case 'topic':
|
|
279
292
|
return null;
|
|
@@ -359,7 +372,96 @@ export class ChannelStore {
|
|
|
359
372
|
// synchronously — nothing can park here.
|
|
360
373
|
void this.send(channel, value);
|
|
361
374
|
}
|
|
375
|
+
/**
|
|
376
|
+
* External-reader subscription (spec 06, External Subscriptions). Delivery
|
|
377
|
+
* by mode: `topic` is a persistent non-lossy tap (per-subscriber bounded
|
|
378
|
+
* buffer, no replay of pre-subscribe values), `queue` competes with internal
|
|
379
|
+
* `recv` waiters FIFO (external consumption frees parked senders), `value`
|
|
380
|
+
* yields the current value then conflated updates.
|
|
381
|
+
*
|
|
382
|
+
* Channel state is keyed by NAME — `executionId` bounds only the
|
|
383
|
+
* subscription's lifetime (it ends when `closeExecution(executionId)`
|
|
384
|
+
* runs; queued values present at close drain first), never which values
|
|
385
|
+
* are delivered. An id that is never closed yields an unbounded stream
|
|
386
|
+
* the caller ends with `iterator.return()`. External waiters never time
|
|
387
|
+
* out — they end via close instead. The returned iterable owns a single
|
|
388
|
+
* iterator (generator semantics): subscribe again for a second consumer.
|
|
389
|
+
*/
|
|
390
|
+
subscribe(channel, executionId) {
|
|
391
|
+
if (this.closedExecutions.has(executionId)) {
|
|
392
|
+
return CLOSED_ITERABLE;
|
|
393
|
+
}
|
|
394
|
+
const state = this.getOrCreate(channel);
|
|
395
|
+
const subscriber = new ExternalSubscriber({
|
|
396
|
+
channelName: channel.name,
|
|
397
|
+
conflate: state.mode === 'value',
|
|
398
|
+
takeShared: state.mode === 'queue'
|
|
399
|
+
? () => {
|
|
400
|
+
const head = this.dequeueHead(state);
|
|
401
|
+
if (head) {
|
|
402
|
+
return head;
|
|
403
|
+
}
|
|
404
|
+
// capacity-0 edge: hand a parked sender's value straight over.
|
|
405
|
+
const sender = state.pendingSenders.shift();
|
|
406
|
+
if (!sender) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
sender.resolve();
|
|
410
|
+
return {
|
|
411
|
+
value: sender.value,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
: undefined,
|
|
415
|
+
parkShared: state.mode === 'queue'
|
|
416
|
+
? (waiter) => {
|
|
417
|
+
state.queueWaiters.push(waiter);
|
|
418
|
+
return () => {
|
|
419
|
+
const idx = state.queueWaiters.indexOf(waiter);
|
|
420
|
+
if (idx >= 0) {
|
|
421
|
+
state.queueWaiters.splice(idx, 1);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
: undefined,
|
|
426
|
+
onFinish: () => {
|
|
427
|
+
state.externalSubscribers.delete(subscriber);
|
|
428
|
+
const byExecution = this.subscribersByExecution.get(executionId);
|
|
429
|
+
if (byExecution) {
|
|
430
|
+
byExecution.delete(erased);
|
|
431
|
+
if (byExecution.size === 0) {
|
|
432
|
+
this.subscribersByExecution.delete(executionId);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
const erased = frameworkCast(subscriber);
|
|
438
|
+
if (state.mode !== 'queue') {
|
|
439
|
+
state.externalSubscribers.add(subscriber);
|
|
440
|
+
}
|
|
441
|
+
if (state.mode === 'value' && state.hasValue) {
|
|
442
|
+
subscriber.push(state.currentValue);
|
|
443
|
+
}
|
|
444
|
+
let byExecution = this.subscribersByExecution.get(executionId);
|
|
445
|
+
if (!byExecution) {
|
|
446
|
+
byExecution = new Set();
|
|
447
|
+
this.subscribersByExecution.set(executionId, byExecution);
|
|
448
|
+
}
|
|
449
|
+
byExecution.add(erased);
|
|
450
|
+
return subscriber;
|
|
451
|
+
}
|
|
452
|
+
/** Shift the queue head and promote the oldest parked sender into the freed slot. */
|
|
453
|
+
dequeueHead(state) {
|
|
454
|
+
if (state.queue.length === 0) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
const head = state.queue.shift();
|
|
458
|
+
this.promotePendingSender(state);
|
|
459
|
+
return {
|
|
460
|
+
value: head,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
362
463
|
getHandle(channel, executionId) {
|
|
464
|
+
this.handleObserved.add(executionId);
|
|
363
465
|
const store = this;
|
|
364
466
|
return {
|
|
365
467
|
get closed() {
|
|
@@ -378,6 +480,190 @@ export class ChannelStore {
|
|
|
378
480
|
};
|
|
379
481
|
}
|
|
380
482
|
closeExecution(executionId) {
|
|
381
|
-
|
|
483
|
+
// Closure is durably recorded only for ids with issued handles — see
|
|
484
|
+
// `handleObserved`. Consequence: a handle taken AFTER an execution
|
|
485
|
+
// completed reads `closed === false`, and a stream subscribed after a
|
|
486
|
+
// stream-only execution completed waits instead of ending; take handles
|
|
487
|
+
// and subscribe before running.
|
|
488
|
+
if (this.handleObserved.has(executionId)) {
|
|
489
|
+
this.closedExecutions.add(executionId);
|
|
490
|
+
}
|
|
491
|
+
const subscribers = this.subscribersByExecution.get(executionId);
|
|
492
|
+
if (!subscribers) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
this.subscribersByExecution.delete(executionId);
|
|
496
|
+
for (const subscriber of subscribers) {
|
|
497
|
+
subscriber.close();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* A root run is (re)starting on this execution id: clear any previous
|
|
502
|
+
* closure so checkpoint-restored and sequentially re-run root contexts get
|
|
503
|
+
* working channels again instead of a permanently poisoned id.
|
|
504
|
+
*/
|
|
505
|
+
openExecution(executionId) {
|
|
506
|
+
this.closedExecutions.delete(executionId);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const MAX_EXTERNAL_BUFFER = 1_000;
|
|
510
|
+
const CLOSED_ITERABLE = {
|
|
511
|
+
[Symbol.asyncIterator]() {
|
|
512
|
+
return this;
|
|
513
|
+
},
|
|
514
|
+
next() {
|
|
515
|
+
return Promise.resolve({
|
|
516
|
+
value: undefined,
|
|
517
|
+
done: true,
|
|
518
|
+
});
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
/**
|
|
522
|
+
* The iterator behind `ChannelStore.subscribe`. Topic/value subscribers are
|
|
523
|
+
* push-fed via `push()`; queue subscribers pull from the shared queue state so
|
|
524
|
+
* they compete fairly with internal `recv` waiters. `close()` (execution
|
|
525
|
+
* completed) lets already-buffered values drain before ending; `return()`
|
|
526
|
+
* (consumer walked away) drops them.
|
|
527
|
+
*/
|
|
528
|
+
class ExternalSubscriber {
|
|
529
|
+
opts;
|
|
530
|
+
buffer = [];
|
|
531
|
+
waiters = [];
|
|
532
|
+
parked = [];
|
|
533
|
+
done = false;
|
|
534
|
+
/** Consumer called return(): stop pulling from the shared queue. */
|
|
535
|
+
detached = false;
|
|
536
|
+
constructor(opts) {
|
|
537
|
+
this.opts = opts;
|
|
538
|
+
}
|
|
539
|
+
[Symbol.asyncIterator]() {
|
|
540
|
+
return this;
|
|
541
|
+
}
|
|
542
|
+
/** Deliver a value to this subscriber (topic/value modes). */
|
|
543
|
+
push(value) {
|
|
544
|
+
if (this.done) {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const waiter = this.waiters.shift();
|
|
548
|
+
if (waiter) {
|
|
549
|
+
waiter({
|
|
550
|
+
value,
|
|
551
|
+
done: false,
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (this.opts.conflate) {
|
|
556
|
+
this.buffer = [
|
|
557
|
+
value,
|
|
558
|
+
];
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (this.buffer.length >= MAX_EXTERNAL_BUFFER) {
|
|
562
|
+
console.warn(`[noetic] Channel '${this.opts.channelName}': external subscriber buffer at capacity (${MAX_EXTERNAL_BUFFER}), dropping oldest value.`);
|
|
563
|
+
this.buffer.shift();
|
|
564
|
+
}
|
|
565
|
+
this.buffer.push(value);
|
|
566
|
+
}
|
|
567
|
+
next() {
|
|
568
|
+
if (this.buffer.length > 0) {
|
|
569
|
+
return Promise.resolve({
|
|
570
|
+
value: this.buffer.shift(),
|
|
571
|
+
done: false,
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// Live shared-queue reads stop at close — close() snapshots the queue
|
|
575
|
+
// into this.buffer, so a closed iterator can never steal values sent to
|
|
576
|
+
// a later execution on the same channel name.
|
|
577
|
+
const taken = this.done ? null : this.opts.takeShared?.();
|
|
578
|
+
if (taken) {
|
|
579
|
+
return Promise.resolve({
|
|
580
|
+
value: taken.value,
|
|
581
|
+
done: false,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
if (this.done) {
|
|
585
|
+
return Promise.resolve({
|
|
586
|
+
value: undefined,
|
|
587
|
+
done: true,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
if (this.opts.parkShared) {
|
|
591
|
+
return this.parkInSharedQueue(this.opts.parkShared);
|
|
592
|
+
}
|
|
593
|
+
return new Promise((resolve) => {
|
|
594
|
+
this.waiters.push(resolve);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return() {
|
|
598
|
+
this.detached = true;
|
|
599
|
+
this.buffer = [];
|
|
600
|
+
this.close();
|
|
601
|
+
return Promise.resolve({
|
|
602
|
+
value: undefined,
|
|
603
|
+
done: true,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/** End the iterator: settle every pending `next()` and unregister. Buffered values still drain. */
|
|
607
|
+
close() {
|
|
608
|
+
if (this.done) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
// Snapshot the shared queue's current contents into the private buffer
|
|
612
|
+
// ("buffered values drain first") — after this the iterator never touches
|
|
613
|
+
// live channel state again. A detached iterator (return()) walked away
|
|
614
|
+
// and drains nothing.
|
|
615
|
+
if (!this.detached && this.opts.takeShared) {
|
|
616
|
+
for (let taken = this.opts.takeShared(); taken; taken = this.opts.takeShared()) {
|
|
617
|
+
this.buffer.push(taken.value);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
this.done = true;
|
|
621
|
+
this.opts.onFinish();
|
|
622
|
+
for (const entry of this.parked.splice(0)) {
|
|
623
|
+
entry.unpark();
|
|
624
|
+
entry.settleDone();
|
|
625
|
+
}
|
|
626
|
+
for (const waiter of this.waiters.splice(0)) {
|
|
627
|
+
waiter({
|
|
628
|
+
value: undefined,
|
|
629
|
+
done: true,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Queue mode: wait in the channel's shared waiter FIFO. No timer attaches,
|
|
635
|
+
* so external waiters never reject with `channel_timeout` — `close()` ends
|
|
636
|
+
* them instead.
|
|
637
|
+
*/
|
|
638
|
+
parkInSharedQueue(parkShared) {
|
|
639
|
+
return new Promise((resolve) => {
|
|
640
|
+
const entry = {
|
|
641
|
+
unpark: () => { },
|
|
642
|
+
settleDone: () => resolve({
|
|
643
|
+
value: undefined,
|
|
644
|
+
done: true,
|
|
645
|
+
}),
|
|
646
|
+
};
|
|
647
|
+
const removeParked = () => {
|
|
648
|
+
const idx = this.parked.indexOf(entry);
|
|
649
|
+
if (idx >= 0) {
|
|
650
|
+
this.parked.splice(idx, 1);
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
entry.unpark = parkShared({
|
|
654
|
+
resolve: (value) => {
|
|
655
|
+
removeParked();
|
|
656
|
+
resolve({
|
|
657
|
+
value,
|
|
658
|
+
done: false,
|
|
659
|
+
});
|
|
660
|
+
},
|
|
661
|
+
reject: () => {
|
|
662
|
+
removeParked();
|
|
663
|
+
entry.settleDone();
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
this.parked.push(entry);
|
|
667
|
+
});
|
|
382
668
|
}
|
|
383
669
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noetic-tools/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Step primitives, interpreter, runtime, and context layer contract for Noetic agents.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
69
|
"@noetic-tools/context": "^1.0.0",
|
|
70
|
-
"@noetic-tools/types": "^
|
|
70
|
+
"@noetic-tools/types": "^3.0.0",
|
|
71
71
|
"@openrouter/agent": "^0.6.0",
|
|
72
72
|
"zod": "^4.0.0"
|
|
73
73
|
},
|