@noetic-tools/core 0.2.1 → 1.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.
Files changed (207) hide show
  1. package/dist/adapters/in-memory-fs-adapter.d.ts +1 -1
  2. package/dist/adapters/in-memory-fs-adapter.js +1 -1
  3. package/dist/adapters/in-memory-shell-adapter.d.ts +1 -1
  4. package/dist/adapters/in-memory-subprocess/manifest-persistence.d.ts +2 -2
  5. package/dist/adapters/in-memory-subprocess/metadata.d.ts +1 -1
  6. package/dist/adapters/in-memory-subprocess/types.d.ts +2 -2
  7. package/dist/adapters/in-memory-subprocess-adapter.d.ts +1 -1
  8. package/dist/adapters/openrouter.d.ts +2 -6
  9. package/dist/adapters/openrouter.js +1 -4
  10. package/dist/builders/channel-builder.d.ts +1 -1
  11. package/dist/builders/control-flow-builders.d.ts +2 -3
  12. package/dist/builders/control-flow-builders.js +1 -2
  13. package/dist/builders/every.d.ts +2 -3
  14. package/dist/builders/every.js +1 -1
  15. package/dist/builders/layer-provides-builders.d.ts +1 -1
  16. package/dist/builders/layer-provides-builders.js +1 -1
  17. package/dist/builders/loop-builder.d.ts +2 -2
  18. package/dist/builders/loop-builder.js +1 -1
  19. package/dist/builders/memory-builder.d.ts +1 -1
  20. package/dist/builders/memory-builder.js +1 -1
  21. package/dist/builders/provide-builder.d.ts +2 -2
  22. package/dist/builders/provide-builder.js +1 -1
  23. package/dist/builders/spawn-builder.d.ts +2 -3
  24. package/dist/builders/spawn-builder.js +1 -1
  25. package/dist/builders/step-builders.d.ts +2 -6
  26. package/dist/builders/step-builders.js +1 -1
  27. package/dist/builders/tool-builder.d.ts +1 -3
  28. package/dist/builders/tool-builder.js +1 -1
  29. package/dist/builders/workflow-hydrator.d.ts +2 -3
  30. package/dist/builders/workflow-hydrator.js +1 -2
  31. package/dist/conditions/conditions.d.ts +2 -3
  32. package/dist/conditions/conditions.js +1 -1
  33. package/dist/harness/agent-harness.d.ts +15 -5
  34. package/dist/harness/agent-harness.js +65 -10
  35. package/dist/harness/deps/memory.d.ts +2 -5
  36. package/dist/harness/deps/memory.js +1 -4
  37. package/dist/harness/deps/types.d.ts +3 -16
  38. package/dist/harness/deps/types.js +1 -1
  39. package/dist/harness/model-call.d.ts +1 -3
  40. package/dist/harness/model-call.js +10 -10
  41. package/dist/harness/model-schema.d.ts +10 -4
  42. package/dist/harness/model-schema.js +9 -1
  43. package/dist/index.d.ts +28 -49
  44. package/dist/index.js +9 -27
  45. package/dist/interpreter/action-deps.d.ts +4 -8
  46. package/dist/interpreter/action-deps.js +2 -6
  47. package/dist/interpreter/action-types.d.ts +3 -8
  48. package/dist/interpreter/action-types.js +1 -1
  49. package/dist/interpreter/collect-tools.d.ts +1 -2
  50. package/dist/interpreter/collect-tools.js +1 -1
  51. package/dist/interpreter/execute-action.js +122 -27
  52. package/dist/interpreter/execute-control.d.ts +1 -2
  53. package/dist/interpreter/execute-control.js +60 -7
  54. package/dist/interpreter/execute.d.ts +2 -3
  55. package/dist/interpreter/execute.js +1 -2
  56. package/dist/interpreter/message-helpers.d.ts +1 -1
  57. package/dist/interpreter/message-helpers.js +1 -1
  58. package/dist/interpreter/step-dispatcher.d.ts +2 -5
  59. package/dist/interpreter/typeguards.d.ts +3 -4
  60. package/dist/interpreter/typeguards.js +1 -1
  61. package/dist/observability/span-impl.d.ts +1 -1
  62. package/dist/observability/trace-exporter.d.ts +1 -1
  63. package/dist/patterns/dynamic-workflow.d.ts +2 -5
  64. package/dist/patterns/dynamic-workflow.js +1 -2
  65. package/dist/patterns/flow.d.ts +2 -2
  66. package/dist/patterns/flow.js +1 -1
  67. package/dist/patterns/interview.d.ts +2 -2
  68. package/dist/patterns/interview.js +1 -1
  69. package/dist/patterns/plans.d.ts +2 -3
  70. package/dist/patterns/plans.js +1 -1
  71. package/dist/patterns/ralph-wiggum.d.ts +2 -3
  72. package/dist/patterns/react.d.ts +2 -3
  73. package/dist/portable/memory-layers.d.ts +1 -7
  74. package/dist/portable/memory-layers.js +1 -7
  75. package/dist/portable.d.ts +2 -2
  76. package/dist/portable.js +2 -2
  77. package/dist/runtime/broadcaster-utils.d.ts +1 -1
  78. package/dist/runtime/channel-store.d.ts +43 -3
  79. package/dist/runtime/channel-store.js +233 -37
  80. package/dist/runtime/context-deps.d.ts +3 -3
  81. package/dist/runtime/context-deps.js +2 -2
  82. package/dist/runtime/context-impl.d.ts +17 -1
  83. package/dist/runtime/context-impl.js +30 -3
  84. package/dist/runtime/context-type-deps.d.ts +2 -11
  85. package/dist/runtime/cwd-helpers.d.ts +1 -1
  86. package/dist/runtime/detached-handle.d.ts +2 -3
  87. package/dist/runtime/detached-handle.js +1 -3
  88. package/dist/runtime/durable/checkpoint-store.d.ts +1 -1
  89. package/dist/runtime/durable/checkpoint-store.js +1 -1
  90. package/dist/runtime/durable/harness-checkpoints.d.ts +2 -5
  91. package/dist/runtime/durable/runnable-loop.d.ts +1 -3
  92. package/dist/runtime/durable/session-seed.d.ts +1 -1
  93. package/dist/runtime/durable/stall-nudge.d.ts +17 -10
  94. package/dist/runtime/durable/stall-nudge.js +29 -10
  95. package/dist/runtime/event-broadcaster.d.ts +10 -2
  96. package/dist/runtime/event-broadcaster.js +43 -19
  97. package/dist/runtime/in-memory-storage.d.ts +1 -1
  98. package/dist/runtime/in-memory-storage.js +1 -1
  99. package/dist/runtime/item-log-impl.d.ts +1 -3
  100. package/dist/runtime/item-log-impl.js +1 -1
  101. package/dist/runtime/message-queue.d.ts +1 -2
  102. package/dist/runtime/session-runner.d.ts +1 -4
  103. package/dist/runtime/session-streams.d.ts +1 -2
  104. package/dist/runtime/session-streams.js +1 -1
  105. package/dist/runtime/step-registry.d.ts +2 -2
  106. package/dist/runtime/step-registry.js +1 -2
  107. package/dist/runtime/tool-memory.d.ts +1 -3
  108. package/dist/types/mutable-context.d.ts +2 -3
  109. package/dist/types/step-registrar.d.ts +1 -1
  110. package/dist/unstable.d.ts +5 -9
  111. package/dist/unstable.js +3 -6
  112. package/dist/until/combinators.d.ts +1 -1
  113. package/dist/until/never.d.ts +1 -1
  114. package/dist/until/predicates.d.ts +2 -2
  115. package/package.json +4 -2
  116. package/dist/errors/noetic-config-error.d.ts +0 -41
  117. package/dist/errors/noetic-config-error.js +0 -44
  118. package/dist/errors/noetic-error.d.ts +0 -18
  119. package/dist/errors/noetic-error.js +0 -54
  120. package/dist/memory/budget.d.ts +0 -23
  121. package/dist/memory/budget.js +0 -117
  122. package/dist/memory/exec-context-factory.d.ts +0 -25
  123. package/dist/memory/exec-context-factory.js +0 -30
  124. package/dist/memory/flow-schema.d.ts +0 -52
  125. package/dist/memory/flow-schema.js +0 -102
  126. package/dist/memory/function-call-utils.d.ts +0 -6
  127. package/dist/memory/function-call-utils.js +0 -27
  128. package/dist/memory/layer-api.d.ts +0 -14
  129. package/dist/memory/layer-api.js +0 -91
  130. package/dist/memory/layer-lifecycle.d.ts +0 -160
  131. package/dist/memory/layer-lifecycle.js +0 -552
  132. package/dist/memory/layer-provides.d.ts +0 -32
  133. package/dist/memory/layer-provides.js +0 -33
  134. package/dist/memory/layer-usage.d.ts +0 -16
  135. package/dist/memory/layer-usage.js +0 -54
  136. package/dist/memory/layers/durable-task-state.d.ts +0 -79
  137. package/dist/memory/layers/durable-task-state.js +0 -118
  138. package/dist/memory/layers/file-reference.d.ts +0 -55
  139. package/dist/memory/layers/file-reference.js +0 -594
  140. package/dist/memory/layers/history-window.d.ts +0 -29
  141. package/dist/memory/layers/history-window.js +0 -114
  142. package/dist/memory/layers/observational-memory.d.ts +0 -53
  143. package/dist/memory/layers/observational-memory.js +0 -109
  144. package/dist/memory/layers/plan.d.ts +0 -55
  145. package/dist/memory/layers/plan.js +0 -374
  146. package/dist/memory/layers/static-content.d.ts +0 -27
  147. package/dist/memory/layers/static-content.js +0 -35
  148. package/dist/memory/layers/steering.d.ts +0 -51
  149. package/dist/memory/layers/steering.js +0 -258
  150. package/dist/memory/layers/temporal.d.ts +0 -91
  151. package/dist/memory/layers/temporal.js +0 -245
  152. package/dist/memory/layers/tool-memory-layer.d.ts +0 -16
  153. package/dist/memory/layers/tool-memory-layer.js +0 -43
  154. package/dist/memory/layers/working-memory.d.ts +0 -44
  155. package/dist/memory/layers/working-memory.js +0 -92
  156. package/dist/memory/projector.d.ts +0 -10
  157. package/dist/memory/projector.js +0 -19
  158. package/dist/memory/scope.d.ts +0 -3
  159. package/dist/memory/scope.js +0 -31
  160. package/dist/memory/strip-unresolved.d.ts +0 -13
  161. package/dist/memory/strip-unresolved.js +0 -35
  162. package/dist/schemas/item.d.ts +0 -35
  163. package/dist/schemas/item.js +0 -155
  164. package/dist/types/channel.d.ts +0 -29
  165. package/dist/types/channel.js +0 -1
  166. package/dist/types/common.d.ts +0 -68
  167. package/dist/types/common.js +0 -1
  168. package/dist/types/context-parts/item-log.d.ts +0 -6
  169. package/dist/types/context-parts/item-log.js +0 -1
  170. package/dist/types/context-parts/layer-usage.d.ts +0 -18
  171. package/dist/types/context-parts/layer-usage.js +0 -1
  172. package/dist/types/context.d.ts +0 -202
  173. package/dist/types/context.js +0 -1
  174. package/dist/types/detached.d.ts +0 -17
  175. package/dist/types/detached.js +0 -7
  176. package/dist/types/error.d.ts +0 -66
  177. package/dist/types/error.js +0 -1
  178. package/dist/types/fs-adapter.d.ts +0 -51
  179. package/dist/types/fs-adapter.js +0 -3
  180. package/dist/types/harness-result.d.ts +0 -43
  181. package/dist/types/harness-result.js +0 -2
  182. package/dist/types/items.d.ts +0 -131
  183. package/dist/types/items.js +0 -2
  184. package/dist/types/memory-context.d.ts +0 -39
  185. package/dist/types/memory-context.js +0 -1
  186. package/dist/types/memory.d.ts +0 -306
  187. package/dist/types/memory.js +0 -13
  188. package/dist/types/observability.d.ts +0 -30
  189. package/dist/types/observability.js +0 -1
  190. package/dist/types/runtime.d.ts +0 -256
  191. package/dist/types/runtime.js +0 -1
  192. package/dist/types/shell-adapter.d.ts +0 -34
  193. package/dist/types/shell-adapter.js +0 -10
  194. package/dist/types/steering.d.ts +0 -89
  195. package/dist/types/steering.js +0 -14
  196. package/dist/types/step.d.ts +0 -231
  197. package/dist/types/step.js +0 -1
  198. package/dist/types/subprocess-adapter.d.ts +0 -157
  199. package/dist/types/subprocess-adapter.js +0 -3
  200. package/dist/types/tool-context.d.ts +0 -34
  201. package/dist/types/tool-context.js +0 -1
  202. package/dist/types/tool.d.ts +0 -64
  203. package/dist/types/tool.js +0 -1
  204. package/dist/util/framework-cast.d.ts +0 -10
  205. package/dist/util/framework-cast.js +0 -16
  206. package/dist/util/message-helpers.d.ts +0 -17
  207. package/dist/util/message-helpers.js +0 -64
@@ -1,9 +1,14 @@
1
- import { NoeticErrorImpl } from '../errors/noetic-error.js';
2
- import { frameworkCast } from '../util/framework-cast.js';
3
- function isExternalChannel(ch) {
4
- return 'external' in ch && ch.external === true;
5
- }
1
+ import { frameworkCast, NoeticErrorImpl } from '@noetic-tools/types';
6
2
  const MAX_TOPIC_TIMEOUT = 300_000; // 5 minutes
3
+ const DEFAULT_SEND_TIMEOUT = 30_000;
4
+ const RESOLVED = Promise.resolve();
5
+ /** Build the `cancelled` error a blocked channel operation rejects with on abort. */
6
+ function cancelledError(signal) {
7
+ return new NoeticErrorImpl({
8
+ kind: 'cancelled',
9
+ reason: typeof signal.reason === 'string' ? signal.reason : undefined,
10
+ });
11
+ }
7
12
  export class ChannelStore {
8
13
  channels = new Map();
9
14
  closedExecutions = new Set();
@@ -17,6 +22,7 @@ export class ChannelStore {
17
22
  queue: [],
18
23
  capacity: channel.capacity ?? 1_000,
19
24
  queueWaiters: [],
25
+ pendingSenders: [],
20
26
  topicSubscribers: new Set(),
21
27
  wakeSubscribers: new Set(),
22
28
  };
@@ -40,14 +46,38 @@ export class ChannelStore {
40
46
  subscribers.delete(callback);
41
47
  };
42
48
  }
43
- send(channel, value) {
49
+ /**
50
+ * Internal-sender write with back-pressure (spec 06). Delivery is
51
+ * synchronous when possible; the returned promise only parks when a queue
52
+ * channel is at capacity:
53
+ *
54
+ * - **value**: store last-write-wins value, drain ALL parked recv waiters,
55
+ * resolve immediately.
56
+ * - **queue**: hand off to a parked recv waiter or push below capacity —
57
+ * resolve immediately. At capacity, park the sender FIFO until a consumer
58
+ * dequeues an item; reject `channel_timeout` after `opts.timeout`
59
+ * (default 30s) or `cancelled` when `opts.signal` aborts.
60
+ * - **topic**: deliver to current subscribers, resolve immediately.
61
+ *
62
+ * External callers use `ChannelHandle.send` (sync, drop-oldest) instead.
63
+ */
64
+ send(channel, value, opts) {
44
65
  const state = this.getOrCreate(channel);
66
+ let result = RESOLVED;
45
67
  switch (state.mode) {
46
68
  case 'value':
47
69
  state.currentValue = value;
48
70
  state.hasValue = true;
71
+ // Value-mode reads are non-consuming (last-write-wins), so once
72
+ // hasValue flips true EVERY parked waiter must be drained — waking
73
+ // only one would strand the rest until channel_timeout while new
74
+ // recv calls succeed instantly. Splice in place: timeout timers and
75
+ // abort listeners close over the array identity.
49
76
  if (state.valueWaiters.length > 0) {
50
- state.valueWaiters.shift().resolve(value);
77
+ const drained = state.valueWaiters.splice(0, state.valueWaiters.length);
78
+ for (const waiter of drained) {
79
+ waiter.resolve(value);
80
+ }
51
81
  }
52
82
  break;
53
83
  case 'queue':
@@ -58,13 +88,8 @@ export class ChannelStore {
58
88
  state.queue.push(value);
59
89
  }
60
90
  else {
61
- // At capacity
62
- console.warn(`[noetic] Channel '${channel.name}': queue at capacity (${state.capacity}), dropping message.`);
63
- if (isExternalChannel(channel)) {
64
- state.queue.shift();
65
- state.queue.push(value);
66
- }
67
- // For internal, drop the new value
91
+ // At capacity — park the sender (back-pressure).
92
+ result = this.parkSender(state, channel.name, value, opts);
68
93
  }
69
94
  break;
70
95
  case 'topic':
@@ -73,30 +98,128 @@ export class ChannelStore {
73
98
  }
74
99
  break;
75
100
  }
76
- if (state.wakeSubscribers.size > 0) {
77
- // Swap in a fresh set before firing so a callback that re-subscribes
78
- // (typical for `every` re-arming on its next iteration) lands on the
79
- // new set and survives until the next send.
80
- const wakers = state.wakeSubscribers;
81
- state.wakeSubscribers = new Set();
82
- for (const wake of wakers) {
83
- wake();
101
+ this.fireWakes(state);
102
+ return result;
103
+ }
104
+ /** Park an internal sender on a full queue until a slot frees (FIFO). */
105
+ parkSender(state, channelName, value, opts) {
106
+ const timeout = opts?.timeout ?? DEFAULT_SEND_TIMEOUT;
107
+ const signal = opts?.signal;
108
+ if (signal?.aborted) {
109
+ return Promise.reject(cancelledError(signal));
110
+ }
111
+ return new Promise((resolve, reject) => {
112
+ let timer = null;
113
+ let removeAbortListener = null;
114
+ const cleanup = () => {
115
+ if (timer) {
116
+ clearTimeout(timer);
117
+ }
118
+ removeAbortListener?.();
119
+ };
120
+ const entry = {
121
+ value,
122
+ resolve: () => {
123
+ cleanup();
124
+ resolve();
125
+ },
126
+ reject: (e) => {
127
+ cleanup();
128
+ reject(e);
129
+ },
130
+ };
131
+ state.pendingSenders.push(entry);
132
+ const removeEntry = () => {
133
+ const idx = state.pendingSenders.indexOf(entry);
134
+ if (idx < 0) {
135
+ return false;
136
+ }
137
+ state.pendingSenders.splice(idx, 1);
138
+ return true;
139
+ };
140
+ if (timeout > 0) {
141
+ timer = setTimeout(() => {
142
+ if (removeEntry()) {
143
+ entry.reject(new NoeticErrorImpl({
144
+ kind: 'channel_timeout',
145
+ channelName,
146
+ timeout,
147
+ }));
148
+ }
149
+ }, timeout);
84
150
  }
151
+ if (signal) {
152
+ const onAbort = () => {
153
+ if (removeEntry()) {
154
+ entry.reject(cancelledError(signal));
155
+ }
156
+ };
157
+ signal.addEventListener('abort', onAbort, {
158
+ once: true,
159
+ });
160
+ removeAbortListener = () => {
161
+ signal.removeEventListener('abort', onAbort);
162
+ };
163
+ }
164
+ });
165
+ }
166
+ /**
167
+ * A dequeue freed a queue slot — shift the oldest parked sender's value
168
+ * into it (FIFO) and resolve that sender's promise.
169
+ */
170
+ promotePendingSender(state) {
171
+ if (state.pendingSenders.length === 0) {
172
+ return;
173
+ }
174
+ if (state.queue.length >= state.capacity) {
175
+ return;
176
+ }
177
+ const sender = state.pendingSenders.shift();
178
+ state.queue.push(sender.value);
179
+ sender.resolve();
180
+ }
181
+ /** Fire-and-reset the one-shot wake subscribers for a channel state. */
182
+ fireWakes(state) {
183
+ if (state.wakeSubscribers.size === 0) {
184
+ return;
185
+ }
186
+ // Swap in a fresh set before firing so a callback that re-subscribes
187
+ // (typical for `every` re-arming on its next iteration) lands on the
188
+ // new set and survives until the next send.
189
+ const wakers = state.wakeSubscribers;
190
+ state.wakeSubscribers = new Set();
191
+ for (const wake of wakers) {
192
+ wake();
85
193
  }
86
194
  }
87
- async recv(channel, timeout = 30_000) {
195
+ async recv(channel, timeout = 30_000, signal) {
196
+ // An already-aborted context must reject promptly rather than park a
197
+ // waiter that nothing will ever wake (spec 09, Cancellation item 2).
198
+ if (signal?.aborted) {
199
+ throw cancelledError(signal);
200
+ }
88
201
  const state = this.getOrCreate(channel);
89
202
  switch (state.mode) {
90
203
  case 'value':
91
204
  if (state.hasValue) {
92
205
  return state.currentValue;
93
206
  }
94
- return this.waitWithTimeout(state.valueWaiters, channel.name, timeout);
95
- case 'queue':
207
+ return this.waitWithTimeout(state.valueWaiters, channel.name, timeout, signal);
208
+ case 'queue': {
96
209
  if (state.queue.length > 0) {
97
- return state.queue.shift();
210
+ const head = state.queue.shift();
211
+ this.promotePendingSender(state);
212
+ return head;
98
213
  }
99
- return this.waitWithTimeout(state.queueWaiters, channel.name, timeout);
214
+ // capacity-0 edge: senders can be parked while the queue is empty —
215
+ // hand the oldest parked value straight to this receiver.
216
+ if (state.pendingSenders.length > 0) {
217
+ const sender = state.pendingSenders.shift();
218
+ sender.resolve();
219
+ return sender.value;
220
+ }
221
+ return this.waitWithTimeout(state.queueWaiters, channel.name, timeout, signal);
222
+ }
100
223
  case 'topic': {
101
224
  // Clamp timeout to prevent indefinite subscriber leaks
102
225
  let effectiveTimeout = timeout;
@@ -105,8 +228,10 @@ export class ChannelStore {
105
228
  effectiveTimeout = MAX_TOPIC_TIMEOUT;
106
229
  }
107
230
  return new Promise((resolve, reject) => {
231
+ let removeAbortListener = null;
108
232
  const timer = setTimeout(() => {
109
233
  state.topicSubscribers.delete(handler);
234
+ removeAbortListener?.();
110
235
  reject(new NoeticErrorImpl({
111
236
  kind: 'channel_timeout',
112
237
  channelName: channel.name,
@@ -116,9 +241,23 @@ export class ChannelStore {
116
241
  const handler = (value) => {
117
242
  clearTimeout(timer);
118
243
  state.topicSubscribers.delete(handler);
244
+ removeAbortListener?.();
119
245
  resolve(value);
120
246
  };
121
247
  state.topicSubscribers.add(handler);
248
+ if (signal) {
249
+ const onAbort = () => {
250
+ clearTimeout(timer);
251
+ state.topicSubscribers.delete(handler);
252
+ reject(cancelledError(signal));
253
+ };
254
+ signal.addEventListener('abort', onAbort, {
255
+ once: true,
256
+ });
257
+ removeAbortListener = () => {
258
+ signal.removeEventListener('abort', onAbort);
259
+ };
260
+ }
122
261
  });
123
262
  }
124
263
  }
@@ -128,32 +267,55 @@ export class ChannelStore {
128
267
  switch (state.mode) {
129
268
  case 'value':
130
269
  return state.hasValue ? state.currentValue : null;
131
- case 'queue':
132
- return state.queue.length > 0 ? state.queue.shift() : null;
270
+ case 'queue': {
271
+ if (state.queue.length > 0) {
272
+ const head = state.queue.shift();
273
+ this.promotePendingSender(state);
274
+ return head;
275
+ }
276
+ return null;
277
+ }
133
278
  case 'topic':
134
279
  return null;
135
280
  }
136
281
  }
137
- waitWithTimeout(waiters, channelName, timeout) {
282
+ waitWithTimeout(waiters, channelName, timeout, signal) {
138
283
  return new Promise((resolve, reject) => {
139
284
  let timer = null;
140
- const wrappedResolve = (v) => {
285
+ let removeAbortListener = null;
286
+ // Symmetric cleanup: whichever path settles the waiter (delivery,
287
+ // timeout, abort) must release both the timer and the abort listener.
288
+ const cleanup = () => {
141
289
  if (timer) {
142
290
  clearTimeout(timer);
143
291
  }
292
+ removeAbortListener?.();
293
+ };
294
+ const wrappedResolve = (v) => {
295
+ cleanup();
144
296
  resolve(v);
145
297
  };
298
+ const wrappedReject = (e) => {
299
+ cleanup();
300
+ reject(e);
301
+ };
146
302
  const entry = {
147
303
  resolve: wrappedResolve,
148
- reject,
304
+ reject: wrappedReject,
149
305
  };
150
306
  waiters.push(entry);
307
+ const removeEntry = () => {
308
+ const idx = waiters.indexOf(entry);
309
+ if (idx < 0) {
310
+ return false;
311
+ }
312
+ waiters.splice(idx, 1);
313
+ return true;
314
+ };
151
315
  if (timeout > 0) {
152
316
  timer = setTimeout(() => {
153
- const idx = waiters.indexOf(entry);
154
- if (idx >= 0) {
155
- waiters.splice(idx, 1);
156
- reject(new NoeticErrorImpl({
317
+ if (removeEntry()) {
318
+ wrappedReject(new NoeticErrorImpl({
157
319
  kind: 'channel_timeout',
158
320
  channelName,
159
321
  timeout,
@@ -161,8 +323,42 @@ export class ChannelStore {
161
323
  }
162
324
  }, timeout);
163
325
  }
326
+ if (signal) {
327
+ const onAbort = () => {
328
+ if (removeEntry()) {
329
+ wrappedReject(cancelledError(signal));
330
+ }
331
+ };
332
+ signal.addEventListener('abort', onAbort, {
333
+ once: true,
334
+ });
335
+ removeAbortListener = () => {
336
+ signal.removeEventListener('abort', onAbort);
337
+ };
338
+ }
164
339
  });
165
340
  }
341
+ /**
342
+ * External-sender write: synchronous, never back-pressured (spec 06,
343
+ * External Sender Back-Pressure). At capacity the OLDEST queued item is
344
+ * dropped (with a warning) so external callers never block.
345
+ */
346
+ sendExternal(channel, value) {
347
+ const state = this.getOrCreate(channel);
348
+ const isFullQueue = state.mode === 'queue' &&
349
+ state.queueWaiters.length === 0 &&
350
+ state.queue.length >= state.capacity;
351
+ if (isFullQueue) {
352
+ console.warn(`[noetic] Channel '${channel.name}': queue at capacity (${state.capacity}), dropping oldest message (external sender).`);
353
+ state.queue.shift();
354
+ state.queue.push(value);
355
+ this.fireWakes(state);
356
+ return;
357
+ }
358
+ // Below capacity (or non-queue mode) the internal path resolves
359
+ // synchronously — nothing can park here.
360
+ void this.send(channel, value);
361
+ }
166
362
  getHandle(channel, executionId) {
167
363
  const store = this;
168
364
  return {
@@ -177,7 +373,7 @@ export class ChannelStore {
177
373
  channelName: channel.name,
178
374
  });
179
375
  }
180
- store.send(channel, value);
376
+ store.sendExternal(channel, value);
181
377
  },
182
378
  };
183
379
  }
@@ -1,4 +1,4 @@
1
- export { buildContextMemory } from '../memory/layer-api.js';
2
- export type { ItemSchemaRegistry } from '../schemas/item.js';
3
- export { defaultItemSchemaRegistry } from '../schemas/item.js';
1
+ export { buildContextMemory } from '@noetic-tools/memory';
2
+ export type { ItemSchemaRegistry } from '@noetic-tools/types';
3
+ export { defaultItemSchemaRegistry } from '@noetic-tools/types';
4
4
  export type { AgentHarnessContract, Channel, ChannelStore, Context, ContextMemory, CwdState, EventBroadcaster, FrontierFrame, FsAdapter, Item, ItemLog, LastLayerUsage, MemoryLayer, Span, StepMeta, TokenUsage, Tool, } from './context-type-deps.js';
@@ -1,2 +1,2 @@
1
- export { buildContextMemory } from '../memory/layer-api.js';
2
- export { defaultItemSchemaRegistry } from '../schemas/item.js';
1
+ export { buildContextMemory } from '@noetic-tools/memory';
2
+ export { defaultItemSchemaRegistry } from '@noetic-tools/types';
@@ -34,6 +34,15 @@ export declare class ContextImpl implements Context<ContextMemory> {
34
34
  private _completed;
35
35
  private _aborted;
36
36
  private _abortReason?;
37
+ /**
38
+ * Per-context abort fan-out. `abort()` fires it so operations blocked on
39
+ * this context (channel `recv` waiters, parked back-pressure senders)
40
+ * reject promptly with `{ kind: 'cancelled' }` instead of hanging until
41
+ * their timeout (spec 09, Cancellation item 2). Each fork/spawn child
42
+ * constructs its own ContextImpl and therefore its own controller —
43
+ * aborting a child never rejects the parent's waiters.
44
+ */
45
+ private readonly _abortController;
37
46
  private _memory?;
38
47
  /**
39
48
  * Stack of steps currently in flight on this context, most-recent last.
@@ -77,7 +86,7 @@ export declare class ContextImpl implements Context<ContextMemory> {
77
86
  recv<T>(ch: Channel<T>, opts?: {
78
87
  timeout?: number;
79
88
  }): Promise<T>;
80
- send<T>(ch: Channel<T>, value: T): void;
89
+ send<T>(ch: Channel<T>, value: T): Promise<void>;
81
90
  tryRecv<T>(ch: Channel<T>): T | null;
82
91
  checkpoint(): Promise<void>;
83
92
  complete<T>(value: T): void;
@@ -85,6 +94,13 @@ export declare class ContextImpl implements Context<ContextMemory> {
85
94
  get completionValue(): unknown;
86
95
  get abortReason(): string | undefined;
87
96
  abort(reason?: string): void;
97
+ /**
98
+ * @internal
99
+ * Abort signal scoped to this context. Channel operations register on it
100
+ * so `abort()` rejects them promptly. Not part of the public Context
101
+ * interface.
102
+ */
103
+ get abortSignal(): AbortSignal;
88
104
  /**
89
105
  * @internal
90
106
  * Push a frame onto the execution frontier. Called by `execute()` at the
@@ -48,6 +48,15 @@ export class ContextImpl {
48
48
  _completed = false;
49
49
  _aborted = false;
50
50
  _abortReason;
51
+ /**
52
+ * Per-context abort fan-out. `abort()` fires it so operations blocked on
53
+ * this context (channel `recv` waiters, parked back-pressure senders)
54
+ * reject promptly with `{ kind: 'cancelled' }` instead of hanging until
55
+ * their timeout (spec 09, Cancellation item 2). Each fork/spawn child
56
+ * constructs its own ContextImpl and therefore its own controller —
57
+ * aborting a child never rejects the parent's waiters.
58
+ */
59
+ _abortController = new AbortController();
51
60
  _memory;
52
61
  /**
53
62
  * Stack of steps currently in flight on this context, most-recent last.
@@ -110,13 +119,18 @@ export class ContextImpl {
110
119
  if (!this.channelStore) {
111
120
  return Promise.reject(new Error('No channel store configured'));
112
121
  }
113
- return this.channelStore.recv(ch, opts?.timeout);
122
+ return this.channelStore.recv(ch, opts?.timeout, this._abortController.signal);
114
123
  }
115
124
  send(ch, value) {
116
125
  if (!this.channelStore) {
117
- throw new Error('No channel store configured');
126
+ return Promise.reject(new Error('No channel store configured'));
118
127
  }
119
- this.channelStore.send(ch, value);
128
+ // Internal sender: back-pressured on full queue channels (default 30s →
129
+ // channel_timeout); aborting this context rejects a parked send with
130
+ // 'cancelled' via the shared abort signal.
131
+ return this.channelStore.send(ch, value, {
132
+ signal: this._abortController.signal,
133
+ });
120
134
  }
121
135
  tryRecv(ch) {
122
136
  if (!this.channelStore) {
@@ -145,6 +159,19 @@ export class ContextImpl {
145
159
  abort(reason) {
146
160
  this._aborted = true;
147
161
  this._abortReason = reason;
162
+ // Reject everything blocked on this context (channel recv waiters,
163
+ // parked back-pressure senders) with { kind: 'cancelled' }. Idempotent —
164
+ // a second abort() leaves the already-aborted signal untouched.
165
+ this._abortController.abort(reason ?? 'context aborted');
166
+ }
167
+ /**
168
+ * @internal
169
+ * Abort signal scoped to this context. Channel operations register on it
170
+ * so `abort()` rejects them promptly. Not part of the public Context
171
+ * interface.
172
+ */
173
+ get abortSignal() {
174
+ return this._abortController.signal;
148
175
  }
149
176
  /**
150
177
  * @internal
@@ -1,14 +1,5 @@
1
- export type { Channel } from '../types/channel.js';
1
+ export type { ContextMemory, MemoryLayer } from '@noetic-tools/memory';
2
+ export type { AgentHarnessContract, Channel, Context, CwdState, FsAdapter, Item, ItemLog, LastLayerUsage, Span, StepMeta, TokenUsage, Tool, } from '@noetic-tools/types';
2
3
  export type { FrontierFrame } from '../types/checkpoint.js';
3
- export type { StepMeta, TokenUsage } from '../types/common.js';
4
- export type { Context, CwdState } from '../types/context.js';
5
- export type { ItemLog } from '../types/context-parts/item-log.js';
6
- export type { LastLayerUsage } from '../types/context-parts/layer-usage.js';
7
- export type { FsAdapter } from '../types/fs-adapter.js';
8
- export type { Item } from '../types/items.js';
9
- export type { ContextMemory, MemoryLayer } from '../types/memory.js';
10
- export type { Span } from '../types/observability.js';
11
- export type { AgentHarnessContract } from '../types/runtime.js';
12
- export type { Tool } from '../types/tool.js';
13
4
  export type { ChannelStore } from './channel-store.js';
14
5
  export type { EventBroadcaster } from './event-broadcaster.js';
@@ -1,4 +1,4 @@
1
- import type { Context, CwdState } from '../types/context.js';
1
+ import type { Context, CwdState } from '@noetic-tools/types';
2
2
  /**
3
3
  * @public Resolve the live cwd for a tool execution. Falls back to the
4
4
  * tool's factory-time cwd (then `process.cwd()`) so partial test contexts
@@ -1,6 +1,5 @@
1
- import type { DetachedHandle } from '../types/detached.js';
2
- import { DetachedStatus } from '../types/detached.js';
3
- import type { SubprocessAdapter, SubprocessHandle } from '../types/subprocess-adapter.js';
1
+ import type { DetachedHandle, SubprocessAdapter, SubprocessHandle } from '@noetic-tools/types';
2
+ import { DetachedStatus } from '@noetic-tools/types';
4
3
  interface DetachedHandleOpts {
5
4
  /** Id surfaced to callers — usually the execution id (childCtx.id in
6
5
  * `detachedSpawn`) so consumers can correlate it with their own state. */
@@ -1,6 +1,4 @@
1
- import { NoeticErrorImpl } from '../errors/noetic-error.js';
2
- import { DetachedStatus } from '../types/detached.js';
3
- import { frameworkCast } from '../util/framework-cast.js';
1
+ import { DetachedStatus, frameworkCast, NoeticErrorImpl } from '@noetic-tools/types';
4
2
  //#endregion
5
3
  //#region Helpers
6
4
  const INITIAL_POLL_MS = 10;
@@ -1,5 +1,5 @@
1
+ import type { StorageAdapter } from '@noetic-tools/memory';
1
2
  import type { CheckpointSnapshot } from '../../types/checkpoint.js';
2
- import type { StorageAdapter } from '../../types/memory.js';
3
3
  export declare const CheckpointKeys: {
4
4
  readonly ExecPrefix: "execution:";
5
5
  readonly SnapshotSuffix: ":snapshot";
@@ -1,4 +1,4 @@
1
- import { NoeticConfigError } from '../../errors/noetic-config-error.js';
1
+ import { NoeticConfigError } from '@noetic-tools/types';
2
2
  import { CheckpointSnapshotSchema } from '../../types/checkpoint.js';
3
3
  //#region Keys
4
4
  /**
@@ -1,8 +1,5 @@
1
- import type { LayerStateStore } from '../../memory/layer-lifecycle.js';
2
- import type { ItemSchemaRegistry } from '../../schemas/item.js';
3
- import type { Context } from '../../types/context.js';
4
- import type { Item } from '../../types/items.js';
5
- import type { MemoryLayer } from '../../types/memory.js';
1
+ import type { LayerStateStore, MemoryLayer } from '@noetic-tools/memory';
2
+ import type { Context, Item, ItemSchemaRegistry } from '@noetic-tools/types';
6
3
  import type { CheckpointStore } from './checkpoint-store.js';
7
4
  /**
8
5
  * Minimum harness surface `captureCheckpoint` / `restoreFromCheckpoint`
@@ -30,9 +30,7 @@
30
30
  * escalation) lives in the caller and composes via the `afterFirstTurn`
31
31
  * hook.
32
32
  */
33
- import type { HarnessResponse } from '../../types/harness-result.js';
34
- import type { ExecuteInput, Item } from '../../types/items.js';
35
- import type { ExecuteOptions, SessionScope } from '../../types/runtime.js';
33
+ import type { ExecuteInput, ExecuteOptions, HarnessResponse, Item, SessionScope } from '@noetic-tools/types';
36
34
  import type { DetachedSignal } from './detached-signal.js';
37
35
  /**
38
36
  * Minimum harness surface the loop calls into. Defining this
@@ -10,7 +10,7 @@
10
10
  * restore a prior session) has one documented, named entrypoint rather
11
11
  * than reaching for the harness method inline.
12
12
  */
13
- import type { Item } from '../../types/items.js';
13
+ import type { Item } from '@noetic-tools/types';
14
14
  /**
15
15
  * Minimum harness surface `seedFromItems` requires. Defined structurally
16
16
  * so tests (and out-of-process children) can pass a stub without casts;
@@ -5,11 +5,14 @@
5
5
  * and without the agent asking for external input — i.e. the agent
6
6
  * stopped without calling either a terminal tool or an ask-user-style
7
7
  * tool. That's a soft failure: rather than wait forever, we nudge the
8
- * agent once to remind it of its options. If the second turn also
9
- * ends in the same stalled shape we escalate: the caller-supplied
10
- * `onStall` callback records the escalation (task pause, metrics,
11
- * whatever the domain demands) and we settle the signal ourselves with
12
- * a canonical "stalled" outcome so the parent unwinds cleanly.
8
+ * agent once to remind it of its options and wait for the nudged turn
9
+ * to actually complete (`harness.execute` is enqueue-only, so the hook
10
+ * awaits `getAgentResponse` — raced against the signal — before
11
+ * re-checking). If the second turn also ends in the same stalled shape
12
+ * we escalate: the caller-supplied `onStall` callback records the
13
+ * escalation (task pause, metrics, whatever the domain demands) and we
14
+ * settle the signal ourselves with a canonical "stalled" outcome so
15
+ * the parent unwinds cleanly.
13
16
  *
14
17
  * The helper factors the state machine out of the loop so callers can
15
18
  * opt in per-runner. Task-specific concerns (how "pending external
@@ -17,7 +20,7 @@
17
20
  * represents a stalled failure) stay with the caller and flow in via
18
21
  * the config.
19
22
  */
20
- import type { InputMessageItem } from '../../types/items.js';
23
+ import type { InputMessageItem } from '@noetic-tools/types';
21
24
  import type { DetachedSignal } from './detached-signal.js';
22
25
  import type { AfterFirstTurnContext, RunnableLoopHarness } from './runnable-loop.js';
23
26
  /**
@@ -74,9 +77,13 @@ export declare function createNudgeMessage(opts: CreateNudgeMessageOpts): InputM
74
77
  * - the caller reports pending external input (`hasPendingExternal`
75
78
  * returns true — e.g. an outstanding ask-user request).
76
79
  *
77
- * Otherwise it sends the nudge message via `harness.execute`, waits
78
- * for microtasks to flush, and re-checks the same two conditions. If
79
- * the agent still hasn't progressed, the hook fires `onStall` and
80
- * resolves the signal with `buildStalledOutcome()`.
80
+ * Otherwise it sends the nudge message via `harness.execute` and waits
81
+ * for the nudged turn to actually complete. `execute()` only enqueues
82
+ * the message — the turn runs asynchronously through the session
83
+ * runner — so the hook awaits `harness.getAgentResponse(threadId)`
84
+ * (raced against `signal.done` so a terminal tool that settles the
85
+ * signal mid-turn short-circuits the wait) before re-checking the same
86
+ * two conditions. If the agent still hasn't progressed, the hook fires
87
+ * `onStall` and resolves the signal with `buildStalledOutcome()`.
81
88
  */
82
89
  export declare function createStallNudgeHook<TOutcome>(opts: StallNudgeOpts<TOutcome>): (ctx: AfterFirstTurnContext) => Promise<void>;