@inixiative/foundry-core 0.2.0 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/foundry-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -15,11 +15,6 @@
15
15
  "dependencies": {
16
16
  "uuidv7": "^1.0.2"
17
17
  },
18
- "devDependencies": {
19
- "@types/bun": "^1.4.2",
20
- "bun-types": "^1.4.2",
21
- "typescript": "^5.7.0"
22
- },
23
18
  "description": "Foundry core engine: context layers, agents, middleware, signals, threads, harness and tracing primitives",
24
19
  "license": "MIT",
25
20
  "files": [
@@ -14,9 +14,9 @@
14
14
  // Types
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
- export type PromptKind = "approval" | "choice" | "input" | "confirm";
18
- export type PromptUrgency = "low" | "normal" | "high" | "critical";
19
- export type PromptStatus = "pending" | "approved" | "rejected" | "expired" | "auto-resolved";
17
+ export type PromptKind = 'approval' | 'choice' | 'input' | 'confirm';
18
+ export type PromptUrgency = 'low' | 'normal' | 'high' | 'critical';
19
+ export type PromptStatus = 'pending' | 'approved' | 'rejected' | 'expired' | 'auto-resolved';
20
20
 
21
21
  export interface ActionOption {
22
22
  readonly id: string;
@@ -27,7 +27,7 @@ export interface ActionOption {
27
27
  }
28
28
 
29
29
  export interface ActionResolution {
30
- readonly by: "human" | "policy" | "timeout";
30
+ readonly by: 'human' | 'policy' | 'timeout';
31
31
  readonly action: string; // "approved", "rejected", or an option ID
32
32
  readonly input?: string; // for "input" kind prompts
33
33
  readonly timestamp: number;
@@ -80,7 +80,10 @@ export type PromptPolicy = (prompt: ActionPrompt) => ActionResolution | null;
80
80
 
81
81
  export class ActionQueue {
82
82
  private _prompts = new Map<string, ActionPrompt>();
83
- private _waiters = new Map<string, { resolve: (r: ActionResolution) => void; timer?: ReturnType<typeof setTimeout> }>();
83
+ private _waiters = new Map<
84
+ string,
85
+ { resolve: (r: ActionResolution) => void; timer?: ReturnType<typeof setTimeout> }
86
+ >();
84
87
  private _listeners: PromptListener[] = [];
85
88
  private _settleListeners: PromptListener[] = [];
86
89
  private _policies: PromptPolicy[] = [];
@@ -103,7 +106,11 @@ export class ActionQueue {
103
106
 
104
107
  private _settled(prompt: ActionPrompt): void {
105
108
  for (const fn of this._settleListeners) {
106
- try { fn(prompt); } catch { /* listener errors don't block */ }
109
+ try {
110
+ fn(prompt);
111
+ } catch {
112
+ /* listener errors don't block */
113
+ }
107
114
  }
108
115
  }
109
116
 
@@ -131,10 +138,10 @@ export class ActionQueue {
131
138
  message: opts.message,
132
139
  options: opts.options,
133
140
  capability: opts.capability,
134
- urgency: opts.urgency ?? "normal",
141
+ urgency: opts.urgency ?? 'normal',
135
142
  expiresAt: opts.timeoutMs ? Date.now() + opts.timeoutMs : undefined,
136
143
  meta: opts.meta,
137
- status: "pending",
144
+ status: 'pending',
138
145
  };
139
146
 
140
147
  this._prompts.set(id, prompt);
@@ -143,29 +150,25 @@ export class ActionQueue {
143
150
  for (const policy of this._policies) {
144
151
  const resolution = policy(prompt);
145
152
  if (resolution) {
146
- prompt.status = "auto-resolved";
153
+ prompt.status = 'auto-resolved';
147
154
  prompt.resolution = resolution;
148
155
  return resolution;
149
156
  }
150
157
  }
151
158
 
152
- // Notify listeners (viewer badge, event stream, etc.)
153
- for (const fn of this._listeners) {
154
- try { fn(prompt); } catch { /* listener errors don't block */ }
155
- }
156
-
157
- // Block until resolved
158
- return new Promise<ActionResolution>((resolve) => {
159
+ // Register the waiter before notifying: a listener may resolve synchronously.
160
+ const settled = new Promise<ActionResolution>((resolve) => {
159
161
  let timer: ReturnType<typeof setTimeout> | undefined;
160
162
 
161
163
  if (opts.timeoutMs) {
162
164
  timer = setTimeout(() => {
165
+ if (prompt.status !== 'pending') return;
163
166
  const resolution: ActionResolution = {
164
- by: "timeout",
165
- action: "rejected",
167
+ by: 'timeout',
168
+ action: 'rejected',
166
169
  timestamp: Date.now(),
167
170
  };
168
- prompt.status = "expired";
171
+ prompt.status = 'expired';
169
172
  prompt.resolution = resolution;
170
173
  this._waiters.delete(id);
171
174
  resolve(resolution);
@@ -175,26 +178,42 @@ export class ActionQueue {
175
178
 
176
179
  this._waiters.set(id, { resolve, timer });
177
180
  });
181
+
182
+ // Notify listeners (viewer badge, event stream, etc.)
183
+ for (const fn of this._listeners) {
184
+ if (prompt.status !== 'pending') break;
185
+ try {
186
+ fn(prompt);
187
+ } catch {
188
+ /* listener errors don't block */
189
+ }
190
+ }
191
+
192
+ return settled;
178
193
  }
179
194
 
180
195
  /**
181
196
  * Resolve a pending prompt. Called by the viewer UI or external systems.
182
197
  */
183
- resolve(promptId: string, action: string, opts?: { by?: "human" | "policy"; input?: string }): boolean {
198
+ resolve(
199
+ promptId: string,
200
+ action: string,
201
+ opts?: { by?: 'human' | 'policy'; input?: string },
202
+ ): boolean {
184
203
  const prompt = this._prompts.get(promptId);
185
- if (!prompt || prompt.status !== "pending") return false;
204
+ if (!prompt || prompt.status !== 'pending') return false;
186
205
 
187
206
  const resolution: ActionResolution = {
188
- by: opts?.by ?? "human",
207
+ by: opts?.by ?? 'human',
189
208
  action,
190
209
  input: opts?.input,
191
210
  timestamp: Date.now(),
192
211
  };
193
212
 
194
- prompt.status = action === "approved" || action === "confirmed" ? "approved" : "rejected";
213
+ prompt.status = action === 'approved' || action === 'confirmed' ? 'approved' : 'rejected';
195
214
  // For choice prompts, mark as approved if any option selected
196
- if (prompt.kind === "choice" && prompt.options?.some((o) => o.id === action)) {
197
- prompt.status = "approved";
215
+ if (prompt.kind === 'choice' && prompt.options?.some((o) => o.id === action)) {
216
+ prompt.status = 'approved';
198
217
  }
199
218
  prompt.resolution = resolution;
200
219
 
@@ -211,7 +230,7 @@ export class ActionQueue {
211
230
 
212
231
  /** Get all pending prompts. */
213
232
  pending(): ActionPrompt[] {
214
- return [...this._prompts.values()].filter((p) => p.status === "pending");
233
+ return [...this._prompts.values()].filter((p) => p.status === 'pending');
215
234
  }
216
235
 
217
236
  /** Get all prompts for a thread (including resolved). */
@@ -223,7 +242,7 @@ export class ActionQueue {
223
242
  pendingCount(threadId?: string): number {
224
243
  let count = 0;
225
244
  for (const p of this._prompts.values()) {
226
- if (p.status === "pending" && (!threadId || p.threadId === threadId)) count++;
245
+ if (p.status === 'pending' && (!threadId || p.threadId === threadId)) count++;
227
246
  }
228
247
  return count;
229
248
  }
@@ -238,7 +257,7 @@ export class ActionQueue {
238
257
  const cutoff = Date.now() - maxAgeMs;
239
258
  let pruned = 0;
240
259
  for (const [id, p] of this._prompts) {
241
- if (p.status !== "pending" && p.timestamp < cutoff) {
260
+ if (p.status !== 'pending' && p.timestamp < cutoff) {
242
261
  this._prompts.delete(id);
243
262
  pruned++;
244
263
  }