@graphorin/agent 0.6.0 → 0.7.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/CHANGELOG.md +96 -0
- package/README.md +31 -11
- package/dist/errors/index.d.ts +17 -4
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +19 -3
- package/dist/errors/index.js.map +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +153 -1930
- package/dist/factory.js.map +1 -1
- package/dist/fanout/index.d.ts +13 -1
- package/dist/fanout/index.d.ts.map +1 -1
- package/dist/fanout/index.js +13 -4
- package/dist/fanout/index.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -9
- package/dist/index.js.map +1 -1
- package/dist/lateral-leak/index.js +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/run-state/index.d.ts +32 -6
- package/dist/run-state/index.d.ts.map +1 -1
- package/dist/run-state/index.js +46 -22
- package/dist/run-state/index.js.map +1 -1
- package/dist/runtime/agent-surface.js +122 -0
- package/dist/runtime/agent-surface.js.map +1 -0
- package/dist/runtime/agent-to-tool.d.ts +51 -0
- package/dist/runtime/agent-to-tool.d.ts.map +1 -0
- package/dist/runtime/agent-to-tool.js +145 -0
- package/dist/runtime/agent-to-tool.js.map +1 -0
- package/dist/runtime/approvals.js +0 -0
- package/dist/runtime/approvals.js.map +1 -0
- package/dist/runtime/dispatch.js +108 -0
- package/dist/runtime/dispatch.js.map +1 -0
- package/dist/runtime/executor-wiring.js +128 -0
- package/dist/runtime/executor-wiring.js.map +1 -0
- package/dist/runtime/fallback-chain.js +139 -0
- package/dist/runtime/fallback-chain.js.map +1 -0
- package/dist/runtime/handoff.js +307 -0
- package/dist/runtime/handoff.js.map +1 -0
- package/dist/runtime/messages.d.ts +22 -0
- package/dist/runtime/messages.d.ts.map +1 -0
- package/dist/runtime/messages.js +204 -0
- package/dist/runtime/messages.js.map +1 -0
- package/dist/runtime/provider-events.js +117 -0
- package/dist/runtime/provider-events.js.map +1 -0
- package/dist/runtime/run-compaction.js +210 -0
- package/dist/runtime/run-compaction.js.map +1 -0
- package/dist/runtime/run-finish.js +48 -0
- package/dist/runtime/run-finish.js.map +1 -0
- package/dist/runtime/run-gates.js +336 -0
- package/dist/runtime/run-gates.js.map +1 -0
- package/dist/runtime/run-init.js +81 -0
- package/dist/runtime/run-init.js.map +1 -0
- package/dist/runtime/run-input.js +46 -0
- package/dist/runtime/run-input.js.map +1 -0
- package/dist/runtime/step-catalogue.js +173 -0
- package/dist/runtime/step-catalogue.js.map +1 -0
- package/dist/runtime/tool-call-walk.js +189 -0
- package/dist/runtime/tool-call-walk.js.map +1 -0
- package/dist/runtime/tool-wiring.js +159 -0
- package/dist/runtime/tool-wiring.js.map +1 -0
- package/dist/tooling/adapters.js +1 -1
- package/dist/tooling/dataflow.js +1 -1
- package/dist/tooling/policy.js +2 -0
- package/dist/tooling/policy.js.map +1 -1
- package/dist/types.d.ts +63 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -20
- package/src/errors/index.ts +320 -0
- package/src/evaluator-optimizer/index.ts +212 -0
- package/src/factory.ts +957 -0
- package/src/fallback/index.ts +108 -0
- package/src/fanout/index.ts +523 -0
- package/src/filters/index.ts +347 -0
- package/src/index.ts +180 -0
- package/src/internal/ids.ts +46 -0
- package/src/internal/usage-accumulator.ts +90 -0
- package/src/lateral-leak/causality-monitor.ts +221 -0
- package/src/lateral-leak/index.ts +35 -0
- package/src/lateral-leak/merge-guard.ts +151 -0
- package/src/lateral-leak/protocol-guard.ts +222 -0
- package/src/preferred-model/index.ts +210 -0
- package/src/progress/index.ts +238 -0
- package/src/run-state/index.ts +607 -0
- package/src/runtime/agent-surface.ts +218 -0
- package/src/runtime/agent-to-tool.ts +323 -0
- package/src/runtime/approvals.ts +0 -0
- package/src/runtime/dispatch.ts +183 -0
- package/src/runtime/executor-wiring.ts +331 -0
- package/src/runtime/fallback-chain.ts +250 -0
- package/src/runtime/handoff.ts +428 -0
- package/src/runtime/messages.ts +309 -0
- package/src/runtime/provider-events.ts +175 -0
- package/src/runtime/run-compaction.ts +288 -0
- package/src/runtime/run-finish.ts +93 -0
- package/src/runtime/run-gates.ts +419 -0
- package/src/runtime/run-init.ts +169 -0
- package/src/runtime/run-input.ts +102 -0
- package/src/runtime/step-catalogue.ts +338 -0
- package/src/runtime/tool-call-walk.ts +301 -0
- package/src/runtime/tool-wiring.ts +218 -0
- package/src/testing/replay-provider.ts +121 -0
- package/src/tooling/adapters.ts +403 -0
- package/src/tooling/catalogue.ts +36 -0
- package/src/tooling/dataflow.ts +171 -0
- package/src/tooling/plan.ts +123 -0
- package/src/tooling/policy.ts +67 -0
- package/src/tooling/registry-build.ts +191 -0
- package/src/types.ts +696 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D6 structured plan / todo tool + attention recitation. The tool is a
|
|
3
|
+
* TodoWrite-style status-flip mutation over the agent's own working
|
|
4
|
+
* plan, journaled in `RunState.todos` so it survives suspend/resume.
|
|
5
|
+
* Attention recitation renders the current plan back into the prompt
|
|
6
|
+
* near the context end each turn (request-only, cache-layout-aware) so
|
|
7
|
+
* the model keeps its objective in focus on long runs (Manus todo.md /
|
|
8
|
+
* lost-in-the-middle evidence).
|
|
9
|
+
*
|
|
10
|
+
* The tool lives in `@graphorin/agent` (not `@graphorin/tools`) because
|
|
11
|
+
* it mutates run state: its `execute` writes through a factory-supplied
|
|
12
|
+
* callback into the active `RunState`.
|
|
13
|
+
*
|
|
14
|
+
* @packageDocumentation
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { TodoItem, Tool } from '@graphorin/core';
|
|
18
|
+
|
|
19
|
+
/** Stable name of the built-in plan tool. */
|
|
20
|
+
export const PLAN_TOOL_NAME = 'update_plan';
|
|
21
|
+
|
|
22
|
+
const STATUS_VALUES = ['pending', 'in_progress', 'completed'] as const;
|
|
23
|
+
|
|
24
|
+
interface PlanToolInput {
|
|
25
|
+
readonly todos: ReadonlyArray<{
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly content: string;
|
|
28
|
+
readonly status: 'pending' | 'in_progress' | 'completed';
|
|
29
|
+
}>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PlanToolOutput {
|
|
33
|
+
readonly count: number;
|
|
34
|
+
readonly completed: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const planInputSchema = {
|
|
38
|
+
parse: (v: unknown): PlanToolInput => v as PlanToolInput,
|
|
39
|
+
safeParse: (v: unknown) => {
|
|
40
|
+
if (typeof v !== 'object' || v === null || !Array.isArray((v as { todos?: unknown }).todos)) {
|
|
41
|
+
return { success: false as const, error: new Error('expected { todos: TodoItem[] }') };
|
|
42
|
+
}
|
|
43
|
+
const todos = (v as { todos: unknown[] }).todos;
|
|
44
|
+
for (const t of todos) {
|
|
45
|
+
if (
|
|
46
|
+
typeof t !== 'object' ||
|
|
47
|
+
t === null ||
|
|
48
|
+
typeof (t as { id?: unknown }).id !== 'string' ||
|
|
49
|
+
typeof (t as { content?: unknown }).content !== 'string' ||
|
|
50
|
+
!STATUS_VALUES.includes(
|
|
51
|
+
(t as { status?: unknown }).status as (typeof STATUS_VALUES)[number],
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
return { success: false as const, error: new Error('invalid TodoItem') };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { success: true as const, data: v as PlanToolInput };
|
|
58
|
+
},
|
|
59
|
+
toJSON: (): Record<string, unknown> => ({
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
todos: {
|
|
63
|
+
type: 'array',
|
|
64
|
+
items: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
id: { type: 'string' },
|
|
68
|
+
content: { type: 'string' },
|
|
69
|
+
status: { type: 'string', enum: [...STATUS_VALUES] },
|
|
70
|
+
},
|
|
71
|
+
required: ['id', 'content', 'status'],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
required: ['todos'],
|
|
76
|
+
}),
|
|
77
|
+
} as unknown as Tool<PlanToolInput, PlanToolOutput>['inputSchema'];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the plan tool. `applyTodos` writes the validated list into the
|
|
81
|
+
* active RunState (the factory closes over `activeRunState`). Pure
|
|
82
|
+
* read-only-ish: it mutates run bookkeeping, not the outside world, so
|
|
83
|
+
* it is `sideEffectClass: 'read-only'` (never a data-flow sink).
|
|
84
|
+
*/
|
|
85
|
+
export function createPlanTool(
|
|
86
|
+
applyTodos: (todos: ReadonlyArray<TodoItem>) => void,
|
|
87
|
+
): Tool<PlanToolInput, PlanToolOutput> {
|
|
88
|
+
return {
|
|
89
|
+
name: PLAN_TOOL_NAME,
|
|
90
|
+
description:
|
|
91
|
+
'Record or update your working plan as a checklist of todos (TodoWrite-style, full replace). Each item has an id, content, and status (pending | in_progress | completed). Keep exactly ONE item in_progress at a time; mark items completed as you finish them. The plan is journaled and recited back to you each turn - use it to stay on task across a long run.',
|
|
92
|
+
inputSchema: planInputSchema,
|
|
93
|
+
sideEffectClass: 'read-only',
|
|
94
|
+
async execute(input): Promise<PlanToolOutput> {
|
|
95
|
+
const todos: TodoItem[] = input.todos.map((t) => ({
|
|
96
|
+
id: t.id,
|
|
97
|
+
content: t.content,
|
|
98
|
+
status: t.status,
|
|
99
|
+
}));
|
|
100
|
+
applyTodos(todos);
|
|
101
|
+
return {
|
|
102
|
+
count: todos.length,
|
|
103
|
+
completed: todos.filter((t) => t.status === 'completed').length,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
} as Tool<PlanToolInput, PlanToolOutput>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Render the attention-recitation block for the current plan. Returns
|
|
111
|
+
* `null` for an empty plan (nothing to recite). The block is appended to
|
|
112
|
+
* the per-step request copy near the END so it rides the last
|
|
113
|
+
* cache anchor and never busts the stable prompt prefix.
|
|
114
|
+
*
|
|
115
|
+
* @stable
|
|
116
|
+
*/
|
|
117
|
+
export function renderPlanRecitation(todos: ReadonlyArray<TodoItem> | undefined): string | null {
|
|
118
|
+
if (todos === undefined || todos.length === 0) return null;
|
|
119
|
+
const mark = (s: TodoItem['status']): string =>
|
|
120
|
+
s === 'completed' ? '[x]' : s === 'in_progress' ? '[~]' : '[ ]';
|
|
121
|
+
const lines = todos.map((t) => `${mark(t.status)} ${t.content}`);
|
|
122
|
+
return `<plan reminder="stay on task; keep one item in progress">\n${lines.join('\n')}\n</plan>`;
|
|
123
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-side adapter for the D4 tool-argument policy (Progent) and the
|
|
3
|
+
* Rule-of-Two capability preset. Compiles `AgentConfig.toolPolicy` /
|
|
4
|
+
* `AgentConfig.ruleOfTwo` into the structural
|
|
5
|
+
* {@link ToolArgumentPolicyGuard} the tool executor consumes plus a
|
|
6
|
+
* capability floor the run loop folds into its single-writer gate.
|
|
7
|
+
*
|
|
8
|
+
* The pure decision engine lives in `@graphorin/security/policy`; this
|
|
9
|
+
* module is the thin binding, mirroring `tooling/dataflow.ts`.
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { isUntrustedTrustClass } from '@graphorin/security/dataflow';
|
|
15
|
+
import {
|
|
16
|
+
buildRuleOfTwoPolicy,
|
|
17
|
+
evaluateToolArgumentPolicy,
|
|
18
|
+
type PolicySideEffectClass,
|
|
19
|
+
type RuleOfTwoProfile,
|
|
20
|
+
type ToolArgumentPolicy,
|
|
21
|
+
} from '@graphorin/security/policy';
|
|
22
|
+
import type { ToolArgumentPolicyGuard } from '@graphorin/tools/executor';
|
|
23
|
+
|
|
24
|
+
export type { RuleOfTwoProfile, ToolArgumentPolicy } from '@graphorin/security/policy';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Compile the agent's opt-in tool-argument policy + Rule-of-Two profile
|
|
28
|
+
* into a single guard. `ruleOfTwo` is compiled first (yielding a base
|
|
29
|
+
* policy + optional read-only floor); an explicit `toolPolicy` is
|
|
30
|
+
* appended so its rules compose - a forbid in either always wins
|
|
31
|
+
* (forbid-before-allow). Returns `{ guard: undefined }` when neither is
|
|
32
|
+
* configured (zero overhead on the default path).
|
|
33
|
+
*/
|
|
34
|
+
export function buildToolArgumentPolicy(
|
|
35
|
+
toolPolicy: ToolArgumentPolicy | undefined,
|
|
36
|
+
ruleOfTwo: RuleOfTwoProfile | undefined,
|
|
37
|
+
): { readonly guard: ToolArgumentPolicyGuard | undefined; readonly capabilityFloor?: 'read-only' } {
|
|
38
|
+
if (toolPolicy === undefined && ruleOfTwo === undefined) {
|
|
39
|
+
return { guard: undefined };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const compiled = ruleOfTwo !== undefined ? buildRuleOfTwoPolicy(ruleOfTwo) : undefined;
|
|
43
|
+
const merged: ToolArgumentPolicy = {
|
|
44
|
+
rules: [...(compiled?.policy.rules ?? []), ...(toolPolicy?.rules ?? [])],
|
|
45
|
+
defaultDenySensitive:
|
|
46
|
+
(compiled?.policy.defaultDenySensitive ?? false) ||
|
|
47
|
+
(toolPolicy?.defaultDenySensitive ?? false),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const guard: ToolArgumentPolicyGuard = {
|
|
51
|
+
evaluate: (input) =>
|
|
52
|
+
evaluateToolArgumentPolicy(merged, {
|
|
53
|
+
toolName: input.toolName,
|
|
54
|
+
sideEffectClass: input.sideEffectClass as PolicySideEffectClass,
|
|
55
|
+
sensitive: input.sensitive,
|
|
56
|
+
// W-101: derived with the same taxonomy the taint engine uses,
|
|
57
|
+
// so the Rule-of-Two untrustedInput leg and dataflow policy can
|
|
58
|
+
// never disagree about what "untrusted source" means.
|
|
59
|
+
untrustedSource: isUntrustedTrustClass(input.trustClass),
|
|
60
|
+
args: input.args,
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return compiled?.capability !== undefined
|
|
65
|
+
? { guard, capabilityFloor: compiled.capability }
|
|
66
|
+
: { guard };
|
|
67
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble a single `@graphorin/tools` {@link ToolRegistry} from every
|
|
3
|
+
* tool source the agent knows about (first-party `config.tools` + skill
|
|
4
|
+
* tools), then resolve cross-source name collisions deterministically.
|
|
5
|
+
*
|
|
6
|
+
* This realises Principle #12 (one registry, one collision policy) and
|
|
7
|
+
* gives `createToolRegistry(...)` its first production call-site
|
|
8
|
+
* (`createAgent(...)` warm-up - see `factory.ts`). The run loop consumes
|
|
9
|
+
* the resulting registry in a later work item (WI-03); `tool_search`
|
|
10
|
+
* uses it in WI-05. This module only *builds* it.
|
|
11
|
+
*
|
|
12
|
+
* Two deliberate fidelity notes (verified against source, not the plan):
|
|
13
|
+
* - **MCP tools are not auto-stamped.** `adaptMCPTools(...)` returns
|
|
14
|
+
* plain `Tool`s; `__source`/`__trustClass` are only ever assigned by
|
|
15
|
+
* the registry's `normaliseTool(...)` at registration time, derived
|
|
16
|
+
* from the `source` passed to `register(...)`. So {@link inferToolSource}
|
|
17
|
+
* honours an explicit `__source` stamp if one is present (forward-
|
|
18
|
+
* compatible with a dedicated `mcp` config hook / re-registered
|
|
19
|
+
* `ResolvedTool`s) and otherwise treats the tool as first-party. The
|
|
20
|
+
* MCP tool's baked-in `sandboxPolicy` / `inboundSanitization` are
|
|
21
|
+
* preserved regardless (operator override > trust-class default).
|
|
22
|
+
* - **Skill tool-stamping lives here, not in `@graphorin/skills`.** The
|
|
23
|
+
* skills loader defers it (it does not depend on a tools registry);
|
|
24
|
+
* the agent - which depends on both - stamps each inline skill tool
|
|
25
|
+
* via `stampSkillTool(...)`.
|
|
26
|
+
*
|
|
27
|
+
* @packageDocumentation
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { Tool, ToolSource } from '@graphorin/core';
|
|
31
|
+
import type { SkillMetadata } from '@graphorin/skills';
|
|
32
|
+
import { stampSkillTool } from '@graphorin/skills';
|
|
33
|
+
import {
|
|
34
|
+
type CollisionContext,
|
|
35
|
+
type CollisionResolution,
|
|
36
|
+
type CollisionStrategy,
|
|
37
|
+
createToolRegistry,
|
|
38
|
+
type ToolAuditEvent,
|
|
39
|
+
type ToolRegistry,
|
|
40
|
+
type ToolSearchEmbedder,
|
|
41
|
+
} from '@graphorin/tools/registry';
|
|
42
|
+
|
|
43
|
+
import type { SkillsRegistryLike } from '../types.js';
|
|
44
|
+
|
|
45
|
+
/** The source attributed to an unstamped first-party `config.tools` entry. */
|
|
46
|
+
const FIRST_PARTY_SOURCE: ToolSource = Object.freeze({ kind: 'first-party' });
|
|
47
|
+
|
|
48
|
+
/** The five `ToolSource.kind` discriminants (used to validate a stamp). */
|
|
49
|
+
const TOOL_SOURCE_KINDS: ReadonlySet<string> = new Set([
|
|
50
|
+
'first-party',
|
|
51
|
+
'built-in',
|
|
52
|
+
'skill',
|
|
53
|
+
'mcp',
|
|
54
|
+
'web-search',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** Options for {@link buildToolRegistry}. */
|
|
58
|
+
export interface BuildToolRegistryOptions {
|
|
59
|
+
/** First-party (and any pre-stamped) tools, i.e. `config.tools`. */
|
|
60
|
+
readonly tools?: ReadonlyArray<Tool<unknown, unknown, unknown>>;
|
|
61
|
+
/**
|
|
62
|
+
* The agent's skill registry (`config.skills`). Only entries that
|
|
63
|
+
* structurally match the {@link SkillLike} surface contribute tools;
|
|
64
|
+
* non-inline skill sources expose `tools(): []` and are a no-op here.
|
|
65
|
+
*/
|
|
66
|
+
readonly skills?: SkillsRegistryLike;
|
|
67
|
+
/** Collision-resolution strategy. Default `'auto-prefix'`. */
|
|
68
|
+
readonly collisionStrategy?: CollisionStrategy;
|
|
69
|
+
/**
|
|
70
|
+
* Context tag carried on the collision audit rows + used for the
|
|
71
|
+
* `'priority'` tie-break boost. Default `{ source: first-party }`.
|
|
72
|
+
*/
|
|
73
|
+
readonly collisionContext?: CollisionContext;
|
|
74
|
+
/** Audit sink forwarded to the registry (collision + classification rows). */
|
|
75
|
+
readonly emitAudit?: (event: ToolAuditEvent) => void;
|
|
76
|
+
/** Semantic-search embedder. Passed through; consumed by `tool_search` (WI-05). */
|
|
77
|
+
readonly embedder?: ToolSearchEmbedder;
|
|
78
|
+
/** Cosine threshold for the semantic search stage (WI-05). */
|
|
79
|
+
readonly semanticScoreThreshold?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Outcome of {@link buildToolRegistry}. */
|
|
83
|
+
export interface BuildToolRegistryResult {
|
|
84
|
+
/** The assembled registry, post collision-resolution. */
|
|
85
|
+
readonly registry: ToolRegistry;
|
|
86
|
+
/** The collision resolutions (also emitted on the audit sink). */
|
|
87
|
+
readonly resolutions: ReadonlyArray<CollisionResolution>;
|
|
88
|
+
/** Count of tools registered across every source. */
|
|
89
|
+
readonly registered: number;
|
|
90
|
+
/** Count of `skills.list()` entries skipped for not matching the `Skill` surface. */
|
|
91
|
+
readonly skippedSkillEntries: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Build and collision-resolve a {@link ToolRegistry} from the supplied
|
|
96
|
+
* tool sources.
|
|
97
|
+
*
|
|
98
|
+
* Registration order (which seeds the `'registration-order'` tie-break)
|
|
99
|
+
* is: every `tools` entry, then every inline tool of every skill.
|
|
100
|
+
* Throws whatever `register(...)` throws for a malformed tool
|
|
101
|
+
* (`InvalidExampleError`, `InvalidPreferredModelError`,
|
|
102
|
+
* `InvalidSideEffectClassError`) - the registry is the validation
|
|
103
|
+
* authority, so a bad tool fails fast at build time.
|
|
104
|
+
*
|
|
105
|
+
* @stable
|
|
106
|
+
*/
|
|
107
|
+
export function buildToolRegistry(options: BuildToolRegistryOptions = {}): BuildToolRegistryResult {
|
|
108
|
+
const registry = createToolRegistry({
|
|
109
|
+
...(options.emitAudit !== undefined ? { emitAudit: options.emitAudit } : {}),
|
|
110
|
+
...(options.embedder !== undefined ? { embedder: options.embedder } : {}),
|
|
111
|
+
...(options.semanticScoreThreshold !== undefined
|
|
112
|
+
? { semanticScoreThreshold: options.semanticScoreThreshold }
|
|
113
|
+
: {}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let registered = 0;
|
|
117
|
+
|
|
118
|
+
// 1. First-party / pre-stamped tools (`config.tools`).
|
|
119
|
+
for (const tool of options.tools ?? []) {
|
|
120
|
+
registry.register(tool, inferToolSource(tool));
|
|
121
|
+
registered += 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 2. Skill tools. The agent accepts a loose `SkillsRegistryLike`, so
|
|
125
|
+
// each `list()` entry is narrowed structurally before stamping.
|
|
126
|
+
let skippedSkillEntries = 0;
|
|
127
|
+
for (const entry of options.skills?.list?.() ?? []) {
|
|
128
|
+
if (!isSkillLike(entry)) {
|
|
129
|
+
skippedSkillEntries += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
for (const tool of entry.tools()) {
|
|
133
|
+
const stamped = stampSkillTool(tool, entry);
|
|
134
|
+
registry.register(stamped.tool, stamped.source);
|
|
135
|
+
registered += 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 3. Resolve collisions deterministically (first-party wins; losers
|
|
140
|
+
// are namespaced). `assertNoDuplicates(strategy, ctx)` emits a
|
|
141
|
+
// `tool:collision:*` audit row per resolution.
|
|
142
|
+
const resolutions = registry.assertNoDuplicates(
|
|
143
|
+
options.collisionStrategy ?? 'auto-prefix',
|
|
144
|
+
options.collisionContext ?? { source: FIRST_PARTY_SOURCE },
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
return Object.freeze({ registry, resolutions, registered, skippedSkillEntries });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Derive the {@link ToolSource} for a `config.tools` entry. Honours an
|
|
152
|
+
* explicit, well-formed `__source` stamp when present (so re-registered
|
|
153
|
+
* `ResolvedTool`s and a future `mcp` config hook keep their provenance);
|
|
154
|
+
* otherwise the tool is first-party. See the module doc on why MCP tools
|
|
155
|
+
* are *not* auto-detected here.
|
|
156
|
+
*/
|
|
157
|
+
function inferToolSource(tool: Tool<unknown, unknown, unknown>): ToolSource {
|
|
158
|
+
const stamped = (tool as { readonly __source?: unknown }).__source;
|
|
159
|
+
return isToolSource(stamped) ? stamped : FIRST_PARTY_SOURCE;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Structural guard: a well-formed {@link ToolSource} discriminated union. */
|
|
163
|
+
function isToolSource(value: unknown): value is ToolSource {
|
|
164
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
165
|
+
const kind = (value as { readonly kind?: unknown }).kind;
|
|
166
|
+
return typeof kind === 'string' && TOOL_SOURCE_KINDS.has(kind);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Minimal structural view of a `@graphorin/skills` `Skill` - exactly the
|
|
171
|
+
* members {@link buildToolRegistry} reads. The agent's `SkillsRegistryLike`
|
|
172
|
+
* is intentionally loose (`list?(): unknown[]`), so entries are validated
|
|
173
|
+
* at runtime before being treated as skills.
|
|
174
|
+
*/
|
|
175
|
+
interface SkillLike {
|
|
176
|
+
readonly metadata: SkillMetadata;
|
|
177
|
+
tools(): ReadonlyArray<Tool<unknown, unknown, unknown>>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Structural guard for the {@link SkillLike} surface. */
|
|
181
|
+
function isSkillLike(value: unknown): value is SkillLike {
|
|
182
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
183
|
+
const metadata = (value as { readonly metadata?: unknown }).metadata;
|
|
184
|
+
const metadataOk =
|
|
185
|
+
typeof metadata === 'object' &&
|
|
186
|
+
metadata !== null &&
|
|
187
|
+
typeof (metadata as { readonly name?: unknown }).name === 'string' &&
|
|
188
|
+
typeof (metadata as { readonly graphorinTrustLevel?: unknown }).graphorinTrustLevel ===
|
|
189
|
+
'string';
|
|
190
|
+
return metadataOk && typeof (value as { readonly tools?: unknown }).tools === 'function';
|
|
191
|
+
}
|