@inixiative/foundry-core 0.2.0 → 0.4.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/src/tools.ts CHANGED
@@ -18,9 +18,9 @@
18
18
  // without bloating their context with tool descriptions upfront
19
19
  // ---------------------------------------------------------------------------
20
20
 
21
- import type { Capability } from "./capability";
22
- import type { OwnershipScope } from "./scope";
23
- import type { ToolDefinition } from "./types";
21
+ import type { Capability } from './capability';
22
+ import type { OwnershipScope } from './scope';
23
+ import type { ToolDefinition } from './types';
24
24
 
25
25
  // ---------------------------------------------------------------------------
26
26
  // Tool result — every tool returns this wrapper
@@ -47,7 +47,11 @@ export interface ToolCallObservation {
47
47
  /** 1-based position of this call within its dispatch. */
48
48
  readonly sequence: number;
49
49
  /** Original lengths of any field that was cut to its bound. */
50
- readonly truncated?: { readonly input?: number; readonly output?: number; readonly error?: number };
50
+ readonly truncated?: {
51
+ readonly input?: number;
52
+ readonly output?: number;
53
+ readonly error?: number;
54
+ };
51
55
  }
52
56
 
53
57
  export interface ToolResult<T = unknown> {
@@ -93,7 +97,7 @@ export interface NavigateOpts {
93
97
 
94
98
  export interface BrowserTool {
95
99
  readonly id: string;
96
- readonly kind: "browser";
100
+ readonly kind: 'browser';
97
101
 
98
102
  /** Required capabilities for each operation. */
99
103
  readonly capabilities: {
@@ -137,13 +141,13 @@ export interface BrowserTool {
137
141
 
138
142
  export interface ApiRequest {
139
143
  url: string;
140
- method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
144
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';
141
145
  headers?: Record<string, string>;
142
146
  body?: unknown;
143
147
  /** Timeout in ms. Default: 30000. */
144
148
  timeout?: number;
145
149
  /** Parse response body as this type. Default: "json". */
146
- responseType?: "json" | "text" | "buffer";
150
+ responseType?: 'json' | 'text' | 'buffer';
147
151
  }
148
152
 
149
153
  export interface ApiResponse<T = unknown> {
@@ -172,7 +176,7 @@ export interface ApiToolConfig {
172
176
 
173
177
  export interface ApiTool {
174
178
  readonly id: string;
175
- readonly kind: "api";
179
+ readonly kind: 'api';
176
180
 
177
181
  /** Required capability. */
178
182
  readonly capability: Capability;
@@ -184,16 +188,30 @@ export interface ApiTool {
184
188
  request<T = unknown>(req: ApiRequest): Promise<ToolResult<ApiResponse<T>>>;
185
189
 
186
190
  /** Convenience: GET request. */
187
- get<T = unknown>(url: string, headers?: Record<string, string>): Promise<ToolResult<ApiResponse<T>>>;
191
+ get<T = unknown>(
192
+ url: string,
193
+ headers?: Record<string, string>,
194
+ ): Promise<ToolResult<ApiResponse<T>>>;
188
195
 
189
196
  /** Convenience: POST request with JSON body. */
190
- post<T = unknown>(url: string, body: unknown, headers?: Record<string, string>): Promise<ToolResult<ApiResponse<T>>>;
197
+ post<T = unknown>(
198
+ url: string,
199
+ body: unknown,
200
+ headers?: Record<string, string>,
201
+ ): Promise<ToolResult<ApiResponse<T>>>;
191
202
 
192
203
  /** Convenience: PUT request with JSON body. */
193
- put<T = unknown>(url: string, body: unknown, headers?: Record<string, string>): Promise<ToolResult<ApiResponse<T>>>;
204
+ put<T = unknown>(
205
+ url: string,
206
+ body: unknown,
207
+ headers?: Record<string, string>,
208
+ ): Promise<ToolResult<ApiResponse<T>>>;
194
209
 
195
210
  /** Convenience: DELETE request. */
196
- delete<T = unknown>(url: string, headers?: Record<string, string>): Promise<ToolResult<ApiResponse<T>>>;
211
+ delete<T = unknown>(
212
+ url: string,
213
+ headers?: Record<string, string>,
214
+ ): Promise<ToolResult<ApiResponse<T>>>;
197
215
  }
198
216
 
199
217
  // ---------------------------------------------------------------------------
@@ -237,7 +255,7 @@ export interface ShellOpts {
237
255
 
238
256
  export interface ShellTool {
239
257
  readonly id: string;
240
- readonly kind: "shell";
258
+ readonly kind: 'shell';
241
259
 
242
260
  /** Required capability. */
243
261
  readonly capability: Capability;
@@ -277,7 +295,7 @@ export interface ScriptOpts {
277
295
 
278
296
  export interface ScriptTool {
279
297
  readonly id: string;
280
- readonly kind: "script";
298
+ readonly kind: 'script';
281
299
 
282
300
  /** Required capability. */
283
301
  readonly capability: Capability;
@@ -307,7 +325,7 @@ export interface ScriptTool {
307
325
  * Entries without a visibility are unowned legacy records: hidden from every
308
326
  * scoped read unless a source explicitly opts in.
309
327
  */
310
- export type MemoryVisibility = "thread" | "project" | "global";
328
+ export type MemoryVisibility = 'thread' | 'project' | 'global';
311
329
 
312
330
  export interface MemoryEntry {
313
331
  id: string;
@@ -333,7 +351,7 @@ export interface MemorySearchOpts {
333
351
 
334
352
  export interface MemoryTool {
335
353
  readonly id: string;
336
- readonly kind: "memory";
354
+ readonly kind: 'memory';
337
355
  /** The underlying system name (e.g., "file", "sqlite", "redis", "supermemory"). */
338
356
  readonly system: string;
339
357
 
@@ -375,7 +393,7 @@ export interface MemoryTool {
375
393
  export type Tool = BrowserTool | ApiTool | ShellTool | ScriptTool | MemoryTool;
376
394
 
377
395
  /** Tool kind discriminator. */
378
- export type ToolKind = Tool["kind"];
396
+ export type ToolKind = Tool['kind'];
379
397
 
380
398
  /** Metadata about a registered tool (what agents see before using it). */
381
399
  export interface ToolInfo {
@@ -406,19 +424,15 @@ export class ToolRegistry {
406
424
  this._tools.set(tool.id, tool);
407
425
 
408
426
  const capabilities: Capability[] = [];
409
- if (tool.kind === "browser") {
427
+ if (tool.kind === 'browser') {
410
428
  capabilities.push(
411
429
  tool.capabilities.navigate,
412
430
  tool.capabilities.interact,
413
431
  tool.capabilities.execute,
414
432
  tool.capabilities.screenshot,
415
433
  );
416
- } else if (tool.kind === "memory") {
417
- capabilities.push(
418
- tool.capabilities.read,
419
- tool.capabilities.write,
420
- tool.capabilities.delete,
421
- );
434
+ } else if (tool.kind === 'memory') {
435
+ capabilities.push(tool.capabilities.read, tool.capabilities.write, tool.capabilities.delete);
422
436
  } else {
423
437
  capabilities.push(tool.capability);
424
438
  }
@@ -472,10 +486,8 @@ export class ToolRegistry {
472
486
  /** Get a compact summary string for injecting into agent context. */
473
487
  summary(): string {
474
488
  const tools = this.list();
475
- if (tools.length === 0) return "No tools available.";
476
- return tools
477
- .map((t) => `- ${t.id} (${t.kind}): ${t.description}`)
478
- .join("\n");
489
+ if (tools.length === 0) return 'No tools available.';
490
+ return tools.map((t) => `- ${t.id} (${t.kind}): ${t.description}`).join('\n');
479
491
  }
480
492
 
481
493
  /**
@@ -489,128 +501,132 @@ export class ToolRegistry {
489
501
  const desc = this._info.get(id)?.description ?? id;
490
502
 
491
503
  switch (tool.kind) {
492
- case "shell":
504
+ case 'shell':
493
505
  defs.push({
494
506
  name: `${id}_exec`,
495
507
  description: `[${id}] ${desc} — execute a shell command`,
496
508
  inputSchema: {
497
- type: "object",
509
+ type: 'object',
498
510
  properties: {
499
- command: { type: "string", description: "Shell command to execute" },
511
+ command: { type: 'string', description: 'Shell command to execute' },
500
512
  },
501
- required: ["command"],
513
+ required: ['command'],
502
514
  },
503
515
  });
504
516
  break;
505
517
 
506
- case "memory":
518
+ case 'memory':
507
519
  defs.push(
508
520
  {
509
521
  name: `${id}_search`,
510
522
  description: `[${id}] Search ${desc}`,
511
523
  inputSchema: {
512
- type: "object",
524
+ type: 'object',
513
525
  properties: {
514
- query: { type: "string", description: "Search query" },
515
- kind: { type: "string", description: "Filter by entry kind (optional)" },
516
- limit: { type: "number", description: "Max results (default: 20)" },
526
+ query: { type: 'string', description: 'Search query' },
527
+ kind: { type: 'string', description: 'Filter by entry kind (optional)' },
528
+ limit: { type: 'number', description: 'Max results (default: 20)' },
517
529
  },
518
- required: ["query"],
530
+ required: ['query'],
519
531
  },
520
532
  },
521
533
  {
522
534
  name: `${id}_get`,
523
535
  description: `[${id}] Get entry by ID from ${desc}`,
524
536
  inputSchema: {
525
- type: "object",
526
- properties: { id: { type: "string" } },
527
- required: ["id"],
537
+ type: 'object',
538
+ properties: { id: { type: 'string' } },
539
+ required: ['id'],
528
540
  },
529
541
  },
530
542
  {
531
543
  name: `${id}_write`,
532
544
  description: `[${id}] Write an entry to ${desc}`,
533
545
  inputSchema: {
534
- type: "object",
546
+ type: 'object',
535
547
  properties: {
536
- id: { type: "string" },
537
- kind: { type: "string" },
538
- content: { type: "string" },
548
+ id: { type: 'string' },
549
+ kind: { type: 'string' },
550
+ content: { type: 'string' },
539
551
  visibility: {
540
- type: "string",
541
- enum: ["thread", "project", "global"],
542
- description: "Who may read it. Default: thread (private). project/global publish it deliberately.",
552
+ type: 'string',
553
+ enum: ['thread', 'project', 'global'],
554
+ description:
555
+ 'Who may read it. Default: thread (private). project/global publish it deliberately.',
543
556
  },
544
557
  },
545
- required: ["id", "kind", "content"],
558
+ required: ['id', 'kind', 'content'],
546
559
  },
547
560
  },
548
561
  );
549
562
  break;
550
563
 
551
- case "script":
564
+ case 'script':
552
565
  defs.push({
553
566
  name: `${id}_evaluate`,
554
567
  description: `[${id}] ${desc} — execute code and return result`,
555
568
  inputSchema: {
556
- type: "object",
569
+ type: 'object',
557
570
  properties: {
558
- code: { type: "string", description: "TypeScript/JS code to execute. Use 'return' to return a value." },
571
+ code: {
572
+ type: 'string',
573
+ description: "TypeScript/JS code to execute. Use 'return' to return a value.",
574
+ },
559
575
  },
560
- required: ["code"],
576
+ required: ['code'],
561
577
  },
562
578
  });
563
579
  break;
564
580
 
565
- case "api":
581
+ case 'api':
566
582
  defs.push({
567
583
  name: `${id}_request`,
568
584
  description: `[${id}] ${desc} — make an HTTP request`,
569
585
  inputSchema: {
570
- type: "object",
586
+ type: 'object',
571
587
  properties: {
572
- url: { type: "string" },
573
- method: { type: "string", enum: ["GET", "POST", "PUT", "DELETE"] },
574
- body: { description: "Request body (JSON)" },
575
- headers: { type: "object", description: "Additional headers" },
588
+ url: { type: 'string' },
589
+ method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'DELETE'] },
590
+ body: { description: 'Request body (JSON)' },
591
+ headers: { type: 'object', description: 'Additional headers' },
576
592
  },
577
- required: ["url"],
593
+ required: ['url'],
578
594
  },
579
595
  });
580
596
  break;
581
597
 
582
- case "browser":
598
+ case 'browser':
583
599
  defs.push(
584
600
  {
585
601
  name: `${id}_navigate`,
586
602
  description: `[${id}] Navigate to a URL`,
587
603
  inputSchema: {
588
- type: "object",
589
- properties: { url: { type: "string" } },
590
- required: ["url"],
604
+ type: 'object',
605
+ properties: { url: { type: 'string' } },
606
+ required: ['url'],
591
607
  },
592
608
  },
593
609
  {
594
610
  name: `${id}_snapshot`,
595
611
  description: `[${id}] Get page accessibility snapshot`,
596
- inputSchema: { type: "object", properties: {} },
612
+ inputSchema: { type: 'object', properties: {} },
597
613
  },
598
614
  {
599
615
  name: `${id}_click`,
600
616
  description: `[${id}] Click an element`,
601
617
  inputSchema: {
602
- type: "object",
603
- properties: { ref: { type: "string", description: "CSS selector or ref" } },
604
- required: ["ref"],
618
+ type: 'object',
619
+ properties: { ref: { type: 'string', description: 'CSS selector or ref' } },
620
+ required: ['ref'],
605
621
  },
606
622
  },
607
623
  {
608
624
  name: `${id}_evaluate`,
609
625
  description: `[${id}] Execute JavaScript in page`,
610
626
  inputSchema: {
611
- type: "object",
612
- properties: { script: { type: "string" } },
613
- required: ["script"],
627
+ type: 'object',
628
+ properties: { script: { type: 'string' } },
629
+ required: ['script'],
614
630
  },
615
631
  },
616
632
  );
@@ -631,16 +647,16 @@ export class ToolRegistry {
631
647
  opts?: { cwd?: string; scope?: OwnershipScope },
632
648
  ): Promise<ToolResult> {
633
649
  // Parse "toolId_method" format
634
- const lastUnderscore = toolName.lastIndexOf("_");
650
+ const lastUnderscore = toolName.lastIndexOf('_');
635
651
  if (lastUnderscore === -1) {
636
- return { ok: false, summary: `Unknown tool: ${toolName}`, error: "Invalid tool name format" };
652
+ return { ok: false, summary: `Unknown tool: ${toolName}`, error: 'Invalid tool name format' };
637
653
  }
638
654
 
639
655
  // Try progressively shorter prefixes (tool IDs can contain underscores)
640
656
  let tool: Tool | undefined;
641
- let method = "";
657
+ let method = '';
642
658
  for (let i = toolName.length - 1; i >= 0; i--) {
643
- if (toolName[i] === "_") {
659
+ if (toolName[i] === '_') {
644
660
  const candidateId = toolName.slice(0, i);
645
661
  const candidateMethod = toolName.slice(i + 1);
646
662
  if (this._tools.has(candidateId)) {
@@ -652,24 +668,37 @@ export class ToolRegistry {
652
668
  }
653
669
 
654
670
  if (!tool) {
655
- return { ok: false, summary: `Unknown tool: ${toolName}`, error: `No registered tool matches "${toolName}"` };
671
+ return {
672
+ ok: false,
673
+ summary: `Unknown tool: ${toolName}`,
674
+ error: `No registered tool matches "${toolName}"`,
675
+ };
656
676
  }
657
677
 
658
678
  try {
659
679
  switch (tool.kind) {
660
- case "shell":
661
- if (method === "exec") return await tool.exec(input.command as string, { cwd: opts?.cwd });
680
+ case 'shell':
681
+ if (method === 'exec')
682
+ return await tool.exec(input.command as string, { cwd: opts?.cwd });
662
683
  break;
663
684
 
664
- case "memory": {
685
+ case 'memory': {
665
686
  // A scoped dispatch may only reach a tool that can honor the scope.
666
687
  const memory = opts?.scope ? tool.scoped?.(opts.scope) : tool;
667
688
  if (!memory) {
668
- return { ok: false, summary: `Memory tool "${tool.id}" cannot scope reads to a thread`, error: "Unscoped memory tool" };
689
+ return {
690
+ ok: false,
691
+ summary: `Memory tool "${tool.id}" cannot scope reads to a thread`,
692
+ error: 'Unscoped memory tool',
693
+ };
669
694
  }
670
- if (method === "search") return await memory.search(input.query as string, { kind: input.kind as string, limit: input.limit as number });
671
- if (method === "get") return await memory.get(input.id as string);
672
- if (method === "write") {
695
+ if (method === 'search')
696
+ return await memory.search(input.query as string, {
697
+ kind: input.kind as string,
698
+ limit: input.limit as number,
699
+ });
700
+ if (method === 'get') return await memory.get(input.id as string);
701
+ if (method === 'write') {
673
702
  const visibility = input.visibility as MemoryVisibility | undefined;
674
703
  return await memory.write({
675
704
  id: input.id as string,
@@ -682,28 +711,42 @@ export class ToolRegistry {
682
711
  break;
683
712
  }
684
713
 
685
- case "script":
686
- if (method === "evaluate") return await tool.evaluate(input.code as string, { cwd: opts?.cwd });
714
+ case 'script':
715
+ if (method === 'evaluate')
716
+ return await tool.evaluate(input.code as string, { cwd: opts?.cwd });
687
717
  break;
688
718
 
689
- case "api":
690
- if (method === "request") {
719
+ case 'api':
720
+ if (method === 'request') {
691
721
  const api = tool.scoped ? tool.scoped(opts?.scope ?? {}) : tool;
692
- return await api.request({ url: input.url as string, method: (input.method as any) ?? "GET", body: input.body, headers: input.headers as Record<string, string> });
722
+ return await api.request({
723
+ url: input.url as string,
724
+ method: (input.method as any) ?? 'GET',
725
+ body: input.body,
726
+ headers: input.headers as Record<string, string>,
727
+ });
693
728
  }
694
729
  break;
695
730
 
696
- case "browser":
697
- if (method === "navigate") return await tool.navigate(input.url as string);
698
- if (method === "snapshot") return await tool.snapshot();
699
- if (method === "click") return await tool.click(input.ref as string);
700
- if (method === "evaluate") return await tool.evaluate(input.script as string);
731
+ case 'browser':
732
+ if (method === 'navigate') return await tool.navigate(input.url as string);
733
+ if (method === 'snapshot') return await tool.snapshot();
734
+ if (method === 'click') return await tool.click(input.ref as string);
735
+ if (method === 'evaluate') return await tool.evaluate(input.script as string);
701
736
  break;
702
737
  }
703
738
 
704
- return { ok: false, summary: `Unknown method "${method}" for tool kind "${tool.kind}"`, error: "Method not found" };
739
+ return {
740
+ ok: false,
741
+ summary: `Unknown method "${method}" for tool kind "${tool.kind}"`,
742
+ error: 'Method not found',
743
+ };
705
744
  } catch (err) {
706
- return { ok: false, summary: `Tool error: ${(err as Error).message}`, error: (err as Error).message };
745
+ return {
746
+ ok: false,
747
+ summary: `Tool error: ${(err as Error).message}`,
748
+ error: (err as Error).message,
749
+ };
707
750
  }
708
751
  }
709
752
 
package/src/trace.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TokenCounts } from "./token-counts";
1
+ import type { TokenCounts } from './token-counts';
2
2
  /**
3
3
  * A single span in a message's journey through the system.
4
4
  * Each stage (classify, route, dispatch, writeback, middleware) creates a span.
@@ -47,24 +47,24 @@ export interface Span {
47
47
  }
48
48
 
49
49
  export type SpanKind =
50
- | "ingress"
51
- | "classify"
52
- | "route"
53
- | "dispatch"
54
- | "execute"
55
- | "decide"
56
- | "middleware"
57
- | "writeback"
58
- | "egress"
59
- | "fan"
50
+ | 'ingress'
51
+ | 'classify'
52
+ | 'route'
53
+ | 'dispatch'
54
+ | 'execute'
55
+ | 'decide'
56
+ | 'middleware'
57
+ | 'writeback'
58
+ | 'egress'
59
+ | 'fan'
60
60
  | (string & {});
61
61
 
62
- export type SpanStatus = "running" | "ok" | "error";
62
+ export type SpanStatus = 'running' | 'ok' | 'error';
63
63
 
64
- import { newId } from "./id";
64
+ import { newId } from './id';
65
65
 
66
66
  function nextSpanId(): string {
67
- return newId("span");
67
+ return newId('span');
68
68
  }
69
69
 
70
70
  /**
@@ -93,12 +93,12 @@ export class Trace {
93
93
  /** Current active span stack — the innermost is where new children attach. */
94
94
  private _stack: Span[] = [];
95
95
 
96
- constructor(messageId: string, detail?: Pick<Span, "input" | "threadId">) {
97
- this.id = newId("trace");
96
+ constructor(messageId: string, detail?: Pick<Span, 'input' | 'threadId'>) {
97
+ this.id = newId('trace');
98
98
  this.messageId = messageId;
99
99
  this.startedAt = performance.now();
100
100
 
101
- this.root = this._createSpan("ingress", "ingress", undefined, detail);
101
+ this.root = this._createSpan('ingress', 'ingress', undefined, detail);
102
102
  this._stack.push(this.root);
103
103
  }
104
104
 
@@ -122,10 +122,10 @@ export class Trace {
122
122
  span.durationMs = span.endedAt - span.startedAt;
123
123
 
124
124
  if (error) {
125
- span.status = "error";
125
+ span.status = 'error';
126
126
  span.error = error;
127
127
  } else {
128
- span.status = "ok";
128
+ span.status = 'ok';
129
129
  }
130
130
 
131
131
  if (output !== undefined) {
@@ -135,6 +135,27 @@ export class Trace {
135
135
  return span;
136
136
  }
137
137
 
138
+ /**
139
+ * Start a span that runs concurrently with its siblings: a child of the current
140
+ * span that does not become current. End it with endSpan().
141
+ */
142
+ startDetached(name: string, kind: SpanKind, detail?: Partial<Span>): Span {
143
+ const parent = this._stack[this._stack.length - 1];
144
+ const span = this._createSpan(name, kind, parent?.id, detail);
145
+ if (parent) parent.children.push(span);
146
+ return span;
147
+ }
148
+
149
+ /** End a span started with startDetached(). */
150
+ endSpan(span: Span, output?: unknown, error?: unknown): Span {
151
+ span.endedAt = performance.now();
152
+ span.durationMs = span.endedAt - span.startedAt;
153
+ span.status = error ? 'error' : 'ok';
154
+ if (error) span.error = error;
155
+ if (output !== undefined) (span as { output: unknown }).output = output;
156
+ return span;
157
+ }
158
+
138
159
  /** End the entire trace. Closes any remaining open spans. */
139
160
  finish(): void {
140
161
  const now = performance.now();
@@ -145,8 +166,8 @@ export class Trace {
145
166
  span.endedAt = now;
146
167
  span.durationMs = span.endedAt - span.startedAt;
147
168
  }
148
- if (span.status === "running") {
149
- span.status = "ok";
169
+ if (span.status === 'running') {
170
+ span.status = 'ok';
150
171
  }
151
172
  }
152
173
  this.endedAt = now;
@@ -217,7 +238,7 @@ export class Trace {
217
238
  name: string,
218
239
  kind: SpanKind,
219
240
  parentId?: string,
220
- detail?: Partial<Span>
241
+ detail?: Partial<Span>,
221
242
  ): Span {
222
243
  const span: Span = {
223
244
  id: nextSpanId(),
@@ -231,7 +252,7 @@ export class Trace {
231
252
  input: detail?.input,
232
253
  output: undefined,
233
254
  startedAt: performance.now(),
234
- status: "running",
255
+ status: 'running',
235
256
  annotations: detail?.annotations ?? {},
236
257
  children: [],
237
258
  };