@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +31 -11
  3. package/dist/errors/index.d.ts +17 -4
  4. package/dist/errors/index.d.ts.map +1 -1
  5. package/dist/errors/index.js +19 -3
  6. package/dist/errors/index.js.map +1 -1
  7. package/dist/factory.d.ts.map +1 -1
  8. package/dist/factory.js +153 -1930
  9. package/dist/factory.js.map +1 -1
  10. package/dist/fanout/index.d.ts +13 -1
  11. package/dist/fanout/index.d.ts.map +1 -1
  12. package/dist/fanout/index.js +13 -4
  13. package/dist/fanout/index.js.map +1 -1
  14. package/dist/index.d.ts +7 -6
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +11 -9
  17. package/dist/index.js.map +1 -1
  18. package/dist/lateral-leak/index.js +1 -1
  19. package/dist/package.js +6 -0
  20. package/dist/package.js.map +1 -0
  21. package/dist/run-state/index.d.ts +32 -6
  22. package/dist/run-state/index.d.ts.map +1 -1
  23. package/dist/run-state/index.js +46 -22
  24. package/dist/run-state/index.js.map +1 -1
  25. package/dist/runtime/agent-surface.js +122 -0
  26. package/dist/runtime/agent-surface.js.map +1 -0
  27. package/dist/runtime/agent-to-tool.d.ts +51 -0
  28. package/dist/runtime/agent-to-tool.d.ts.map +1 -0
  29. package/dist/runtime/agent-to-tool.js +145 -0
  30. package/dist/runtime/agent-to-tool.js.map +1 -0
  31. package/dist/runtime/approvals.js +0 -0
  32. package/dist/runtime/approvals.js.map +1 -0
  33. package/dist/runtime/dispatch.js +108 -0
  34. package/dist/runtime/dispatch.js.map +1 -0
  35. package/dist/runtime/executor-wiring.js +128 -0
  36. package/dist/runtime/executor-wiring.js.map +1 -0
  37. package/dist/runtime/fallback-chain.js +139 -0
  38. package/dist/runtime/fallback-chain.js.map +1 -0
  39. package/dist/runtime/handoff.js +307 -0
  40. package/dist/runtime/handoff.js.map +1 -0
  41. package/dist/runtime/messages.d.ts +22 -0
  42. package/dist/runtime/messages.d.ts.map +1 -0
  43. package/dist/runtime/messages.js +204 -0
  44. package/dist/runtime/messages.js.map +1 -0
  45. package/dist/runtime/provider-events.js +117 -0
  46. package/dist/runtime/provider-events.js.map +1 -0
  47. package/dist/runtime/run-compaction.js +210 -0
  48. package/dist/runtime/run-compaction.js.map +1 -0
  49. package/dist/runtime/run-finish.js +48 -0
  50. package/dist/runtime/run-finish.js.map +1 -0
  51. package/dist/runtime/run-gates.js +336 -0
  52. package/dist/runtime/run-gates.js.map +1 -0
  53. package/dist/runtime/run-init.js +81 -0
  54. package/dist/runtime/run-init.js.map +1 -0
  55. package/dist/runtime/run-input.js +46 -0
  56. package/dist/runtime/run-input.js.map +1 -0
  57. package/dist/runtime/step-catalogue.js +173 -0
  58. package/dist/runtime/step-catalogue.js.map +1 -0
  59. package/dist/runtime/tool-call-walk.js +189 -0
  60. package/dist/runtime/tool-call-walk.js.map +1 -0
  61. package/dist/runtime/tool-wiring.js +159 -0
  62. package/dist/runtime/tool-wiring.js.map +1 -0
  63. package/dist/tooling/adapters.js +1 -1
  64. package/dist/tooling/dataflow.js +1 -1
  65. package/dist/tooling/policy.js +2 -0
  66. package/dist/tooling/policy.js.map +1 -1
  67. package/dist/types.d.ts +63 -13
  68. package/dist/types.d.ts.map +1 -1
  69. package/package.json +20 -20
  70. package/src/errors/index.ts +320 -0
  71. package/src/evaluator-optimizer/index.ts +212 -0
  72. package/src/factory.ts +957 -0
  73. package/src/fallback/index.ts +108 -0
  74. package/src/fanout/index.ts +523 -0
  75. package/src/filters/index.ts +347 -0
  76. package/src/index.ts +180 -0
  77. package/src/internal/ids.ts +46 -0
  78. package/src/internal/usage-accumulator.ts +90 -0
  79. package/src/lateral-leak/causality-monitor.ts +221 -0
  80. package/src/lateral-leak/index.ts +35 -0
  81. package/src/lateral-leak/merge-guard.ts +151 -0
  82. package/src/lateral-leak/protocol-guard.ts +222 -0
  83. package/src/preferred-model/index.ts +210 -0
  84. package/src/progress/index.ts +238 -0
  85. package/src/run-state/index.ts +607 -0
  86. package/src/runtime/agent-surface.ts +218 -0
  87. package/src/runtime/agent-to-tool.ts +323 -0
  88. package/src/runtime/approvals.ts +0 -0
  89. package/src/runtime/dispatch.ts +183 -0
  90. package/src/runtime/executor-wiring.ts +331 -0
  91. package/src/runtime/fallback-chain.ts +250 -0
  92. package/src/runtime/handoff.ts +428 -0
  93. package/src/runtime/messages.ts +309 -0
  94. package/src/runtime/provider-events.ts +175 -0
  95. package/src/runtime/run-compaction.ts +288 -0
  96. package/src/runtime/run-finish.ts +93 -0
  97. package/src/runtime/run-gates.ts +419 -0
  98. package/src/runtime/run-init.ts +169 -0
  99. package/src/runtime/run-input.ts +102 -0
  100. package/src/runtime/step-catalogue.ts +338 -0
  101. package/src/runtime/tool-call-walk.ts +301 -0
  102. package/src/runtime/tool-wiring.ts +218 -0
  103. package/src/testing/replay-provider.ts +121 -0
  104. package/src/tooling/adapters.ts +403 -0
  105. package/src/tooling/catalogue.ts +36 -0
  106. package/src/tooling/dataflow.ts +171 -0
  107. package/src/tooling/plan.ts +123 -0
  108. package/src/tooling/policy.ts +67 -0
  109. package/src/tooling/registry-build.ts +191 -0
  110. 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
+ }