@graphorin/core 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 (112) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +14 -8
  3. package/dist/channels/channels.d.ts.map +1 -1
  4. package/dist/channels/index.d.ts +2 -2
  5. package/dist/channels/index.js +2 -2
  6. package/dist/channels/pause.d.ts +47 -2
  7. package/dist/channels/pause.d.ts.map +1 -1
  8. package/dist/channels/pause.js +62 -2
  9. package/dist/channels/pause.js.map +1 -1
  10. package/dist/contracts/checkpoint-store.d.ts +97 -1
  11. package/dist/contracts/checkpoint-store.d.ts.map +1 -1
  12. package/dist/contracts/checkpoint-store.js.map +1 -1
  13. package/dist/contracts/index.d.ts +5 -5
  14. package/dist/contracts/index.js +2 -1
  15. package/dist/contracts/memory-store.d.ts +59 -2
  16. package/dist/contracts/memory-store.d.ts.map +1 -1
  17. package/dist/contracts/provider.d.ts +20 -6
  18. package/dist/contracts/provider.d.ts.map +1 -1
  19. package/dist/contracts/session-store.d.ts +10 -2
  20. package/dist/contracts/session-store.d.ts.map +1 -1
  21. package/dist/contracts/tool.d.ts +75 -1
  22. package/dist/contracts/tool.d.ts.map +1 -1
  23. package/dist/contracts/tool.js +57 -0
  24. package/dist/contracts/tool.js.map +1 -0
  25. package/dist/contracts/tracer.d.ts +53 -5
  26. package/dist/contracts/tracer.d.ts.map +1 -1
  27. package/dist/contracts/tracer.js.map +1 -1
  28. package/dist/index.d.ts +14 -9
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +11 -7
  31. package/dist/index.js.map +1 -1
  32. package/dist/package.js +6 -0
  33. package/dist/package.js.map +1 -0
  34. package/dist/types/agent-event-wire.d.ts +74 -0
  35. package/dist/types/agent-event-wire.d.ts.map +1 -0
  36. package/dist/types/agent-event-wire.js +130 -0
  37. package/dist/types/agent-event-wire.js.map +1 -0
  38. package/dist/types/agent-event.d.ts +48 -6
  39. package/dist/types/agent-event.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +3 -2
  41. package/dist/types/index.js +2 -1
  42. package/dist/types/memory.d.ts +11 -0
  43. package/dist/types/memory.d.ts.map +1 -1
  44. package/dist/types/run.d.ts +86 -4
  45. package/dist/types/run.d.ts.map +1 -1
  46. package/dist/types/run.js.map +1 -1
  47. package/dist/types/tool.d.ts +10 -0
  48. package/dist/types/tool.d.ts.map +1 -1
  49. package/dist/types/usage.d.ts +11 -1
  50. package/dist/types/usage.d.ts.map +1 -1
  51. package/dist/types/usage.js.map +1 -1
  52. package/dist/utils/binary-json.d.ts +165 -0
  53. package/dist/utils/binary-json.d.ts.map +1 -0
  54. package/dist/utils/binary-json.js +240 -0
  55. package/dist/utils/binary-json.js.map +1 -0
  56. package/dist/utils/index.d.ts +2 -1
  57. package/dist/utils/index.js +2 -1
  58. package/dist/utils/validation.d.ts +10 -1
  59. package/dist/utils/validation.d.ts.map +1 -1
  60. package/dist/utils/validation.js +1 -1
  61. package/dist/utils/validation.js.map +1 -1
  62. package/package.json +9 -7
  63. package/src/channels/channels.ts +206 -0
  64. package/src/channels/directive.ts +41 -0
  65. package/src/channels/dispatch.ts +37 -0
  66. package/src/channels/durable.ts +151 -0
  67. package/src/channels/index.ts +62 -0
  68. package/src/channels/pause.ts +216 -0
  69. package/src/contracts/auth-token-store.ts +42 -0
  70. package/src/contracts/checkpoint-store.ts +256 -0
  71. package/src/contracts/embedder.ts +42 -0
  72. package/src/contracts/eval-scorer.ts +44 -0
  73. package/src/contracts/index.ts +112 -0
  74. package/src/contracts/local-provider-trust.ts +33 -0
  75. package/src/contracts/logger.ts +61 -0
  76. package/src/contracts/memory-store.ts +187 -0
  77. package/src/contracts/oauth-server-store.ts +78 -0
  78. package/src/contracts/preferred-model.ts +56 -0
  79. package/src/contracts/provider.ts +316 -0
  80. package/src/contracts/reasoning-retention.ts +52 -0
  81. package/src/contracts/redaction-validator.ts +56 -0
  82. package/src/contracts/sandbox.ts +70 -0
  83. package/src/contracts/secret-ref.ts +22 -0
  84. package/src/contracts/secret-value.ts +117 -0
  85. package/src/contracts/secrets-store.ts +90 -0
  86. package/src/contracts/session-store.ts +163 -0
  87. package/src/contracts/token-counter.ts +23 -0
  88. package/src/contracts/tool.ts +397 -0
  89. package/src/contracts/tracer.ts +219 -0
  90. package/src/contracts/trigger-store.ts +40 -0
  91. package/src/index.ts +23 -0
  92. package/src/types/agent-event-wire.ts +193 -0
  93. package/src/types/agent-event.ts +579 -0
  94. package/src/types/handoff.ts +111 -0
  95. package/src/types/index.ts +148 -0
  96. package/src/types/memory.ts +427 -0
  97. package/src/types/message.ts +174 -0
  98. package/src/types/run.ts +312 -0
  99. package/src/types/sensitivity.ts +35 -0
  100. package/src/types/session-scope.ts +18 -0
  101. package/src/types/stop-condition.ts +108 -0
  102. package/src/types/tool-call.ts +24 -0
  103. package/src/types/tool.ts +324 -0
  104. package/src/types/usage.ts +120 -0
  105. package/src/types/workflow-event.ts +132 -0
  106. package/src/utils/assert-never.ts +24 -0
  107. package/src/utils/async-context.ts +55 -0
  108. package/src/utils/binary-json.ts +425 -0
  109. package/src/utils/hash.ts +122 -0
  110. package/src/utils/index.ts +57 -0
  111. package/src/utils/streams.ts +233 -0
  112. package/src/utils/validation.ts +82 -0
@@ -0,0 +1,90 @@
1
+ import type { SessionScope } from '../types/session-scope.js';
2
+ import type { SecretRef } from './secret-ref.js';
3
+ import type { SecretValue } from './secret-value.js';
4
+
5
+ /**
6
+ * Pluggable secret resolver - turns a parsed `SecretRef` into a live
7
+ * `SecretValue`. Concrete resolvers live in `@graphorin/security` (env,
8
+ * keyring, file, encrypted-file, literal, ref, vault) and in optional
9
+ * adapter packages (`@graphorin/secret-1password`, …).
10
+ *
11
+ * @stable
12
+ */
13
+ export interface SecretResolver {
14
+ /** Lowercased URI scheme handled by this resolver (`'env'`, `'op'`, …). */
15
+ readonly scheme: string;
16
+ resolve(ref: SecretRef, ctx?: SecretResolverContext): Promise<SecretValue>;
17
+ }
18
+
19
+ /**
20
+ * Optional context handed to a resolver. Carries the originating tool /
21
+ * agent identifiers so the audit log can attribute the resolution.
22
+ *
23
+ * @stable
24
+ */
25
+ export interface SecretResolverContext {
26
+ readonly toolName?: string;
27
+ readonly agentId?: string;
28
+ readonly runId?: string;
29
+ readonly signal?: AbortSignal;
30
+ }
31
+
32
+ /**
33
+ * Pluggable secret-managing storage. Concrete implementations live in
34
+ * `@graphorin/security` (`KeyringSecretsStore`, `EncryptedFileSecretsStore`,
35
+ * `EnvSecretsStore`, `MemorySecretsStore`).
36
+ *
37
+ * The interface is intentionally narrow: every method either returns a
38
+ * `SecretValue` or a piece of metadata that is safe to log. The raw
39
+ * value is never returned as a `string` from this surface.
40
+ *
41
+ * @stable
42
+ */
43
+ export interface SecretsStore {
44
+ /** Returns the secret if it exists, `null` otherwise. */
45
+ get(key: string, scope?: SessionScope): Promise<SecretValue | null>;
46
+
47
+ /**
48
+ * Returns the secret or throws. Implementations enforce the per-tool
49
+ * `secretsAllowed` ACL: if the current tool context disallows `key`,
50
+ * throw `SecretAccessDeniedError`.
51
+ */
52
+ require(key: string, scope?: SessionScope): Promise<SecretValue>;
53
+
54
+ /**
55
+ * Persist a secret. Implementations auto-wrap a plain string into a
56
+ * `SecretValue` so callers don't have to.
57
+ */
58
+ set(key: string, value: string | SecretValue, opts?: SecretsSetOptions): Promise<void>;
59
+
60
+ delete(key: string, scope?: SessionScope): Promise<void>;
61
+
62
+ /** Returns metadata about every key - never the values themselves. */
63
+ list(scope?: SessionScope): Promise<ReadonlyArray<SecretMetadata>>;
64
+ }
65
+
66
+ /**
67
+ * Optional knobs for `SecretsStore.set(...)`.
68
+ *
69
+ * @stable
70
+ */
71
+ export interface SecretsSetOptions {
72
+ readonly scope?: SessionScope;
73
+ readonly expiresAt?: string;
74
+ readonly tags?: ReadonlyArray<string>;
75
+ }
76
+
77
+ /**
78
+ * Public metadata about a stored secret. Safe to log - never carries the
79
+ * value itself.
80
+ *
81
+ * @stable
82
+ */
83
+ export interface SecretMetadata {
84
+ readonly key: string;
85
+ readonly createdAt: string;
86
+ readonly updatedAt?: string;
87
+ readonly expiresAt?: string;
88
+ readonly tags?: ReadonlyArray<string>;
89
+ readonly source?: string;
90
+ }
@@ -0,0 +1,163 @@
1
+ import type { HandoffRecord } from '../types/handoff.js';
2
+ import type { SessionScope } from '../types/session-scope.js';
3
+
4
+ /**
5
+ * Lightweight session metadata persisted by the sessions package. The
6
+ * actual `session_messages` rows are owned by `MemoryStore` (single source
7
+ * of truth - the sessions package delegates message CRUD to memory).
8
+ *
9
+ * @stable
10
+ */
11
+ export interface SessionMetadata {
12
+ readonly id: string;
13
+ readonly userId: string;
14
+ readonly agentId: string;
15
+ readonly title?: string;
16
+ readonly createdAt: string;
17
+ readonly updatedAt?: string;
18
+ readonly closedAt?: string;
19
+ readonly tags?: ReadonlyArray<string>;
20
+ }
21
+
22
+ /**
23
+ * Agent registry entry. Captures stable metadata about every agent that
24
+ * ever produced a message - so JSONL exports / replays can resolve a
25
+ * `Message.agentId` to a human-readable name even after the agent was
26
+ * renamed or retired.
27
+ *
28
+ * @stable
29
+ */
30
+ export interface AgentRegistryEntry {
31
+ readonly id: string;
32
+ readonly displayName: string;
33
+ readonly registeredAt: string;
34
+ readonly retiredAt?: string;
35
+ readonly tags?: ReadonlyArray<string>;
36
+ }
37
+
38
+ /**
39
+ * Workflow ↔ session mapping row. Lets the server enumerate the
40
+ * workflows attached to a session for resume / replay flows.
41
+ *
42
+ * @stable
43
+ */
44
+ export interface SessionWorkflowRun {
45
+ readonly sessionId: string;
46
+ readonly workflowId: string;
47
+ readonly threadId: string;
48
+ readonly attachedAt: string;
49
+ readonly status: 'running' | 'suspended' | 'completed' | 'failed';
50
+ }
51
+
52
+ /**
53
+ * Session lifecycle audit event. The `@graphorin/sessions` package
54
+ * appends one row per noteworthy lifecycle step (`created`, `closed`,
55
+ * `forked`, `replayed`, `cassette-recorded`, `cassette-replayed`,
56
+ * `commentary-sanitized`, …) plus per-session-handoff. Adapters can
57
+ * surface the rows verbatim from disk.
58
+ *
59
+ * The `metadata` field is intentionally an open record - storage
60
+ * adapters serialize it as JSON. Callers should keep it small and
61
+ * never include secret values.
62
+ *
63
+ * @stable
64
+ */
65
+ export interface SessionAuditEntry {
66
+ readonly id: string;
67
+ readonly sessionId: string;
68
+ readonly action: string;
69
+ readonly at: string;
70
+ readonly actor?: {
71
+ readonly kind: string;
72
+ readonly id: string;
73
+ readonly label?: string;
74
+ };
75
+ readonly metadata?: Readonly<Record<string, unknown>>;
76
+ }
77
+
78
+ /**
79
+ * Pluggable session-metadata storage. Implementations live in the
80
+ * storage adapter packages.
81
+ *
82
+ * @stable
83
+ */
84
+ export interface SessionStore {
85
+ createSession(metadata: SessionMetadata): Promise<void>;
86
+ getSession(sessionId: string): Promise<SessionMetadata | null>;
87
+ listSessions(
88
+ scope: Pick<SessionScope, 'userId' | 'agentId'>,
89
+ ): Promise<ReadonlyArray<SessionMetadata>>;
90
+ updateSession(sessionId: string, patch: Partial<SessionMetadata>): Promise<void>;
91
+ closeSession(sessionId: string, closedAt: string): Promise<void>;
92
+
93
+ registerAgent(entry: AgentRegistryEntry): Promise<void>;
94
+ retireAgent(agentId: string, retiredAt: string): Promise<void>;
95
+ resolveAgent(agentId: string): Promise<AgentRegistryEntry | null>;
96
+
97
+ appendHandoff(sessionId: string, record: HandoffRecord): Promise<void>;
98
+ listHandoffs(sessionId: string): Promise<ReadonlyArray<HandoffRecord>>;
99
+
100
+ attachWorkflowRun(run: SessionWorkflowRun): Promise<void>;
101
+ listWorkflowRuns(sessionId: string): Promise<ReadonlyArray<SessionWorkflowRun>>;
102
+ }
103
+
104
+ /**
105
+ * Optional extension surface for storage adapters that expose the
106
+ * additional capabilities `@graphorin/sessions` consumes.
107
+ * Adapters that opt out leave the property undefined; the sessions
108
+ * facade degrades gracefully (delete becomes retire; audit rows are
109
+ * dropped on the floor with a one-time WARN).
110
+ *
111
+ * Implementations: `SqliteSessionStore` (`@graphorin/store-sqlite`).
112
+ *
113
+ * @stable
114
+ */
115
+ export interface SessionStoreExt extends SessionStore {
116
+ /** Hard-delete an agent. Used by `AgentRegistry.delete(...)`. */
117
+ deleteAgent(agentId: string): Promise<void>;
118
+ /** List all known agents (including retired ones). */
119
+ listAgents(): Promise<ReadonlyArray<AgentRegistryEntry>>;
120
+ /** Update the status of a workflow attachment. */
121
+ updateWorkflowRunStatus(
122
+ sessionId: string,
123
+ workflowId: string,
124
+ threadId: string,
125
+ status: SessionWorkflowRun['status'],
126
+ ): Promise<void>;
127
+ /** Append a session-lifecycle audit row. */
128
+ appendAuditEntry(entry: SessionAuditEntry): Promise<void>;
129
+ /** List recent audit rows for a session, newest-first. */
130
+ listAuditEntries(
131
+ sessionId: string,
132
+ opts?: { readonly limit?: number },
133
+ ): Promise<ReadonlyArray<SessionAuditEntry>>;
134
+ /** Delete audit rows older than the supplied epoch ms. */
135
+ pruneAuditEntries(beforeEpochMs: number): Promise<number>;
136
+ /**
137
+ * Hard-delete a session and cascade its session-owned rows - handoffs,
138
+ * workflow-run attachments, and audit entries (RP-6) - **plus the
139
+ * session's content**: its `session_messages` rows (with their FTS and
140
+ * vector index entries) and any episodes scoped to the session
141
+ * (store-01). The cascade also erases the checkpoints of suspended
142
+ * runs (W-005): `workflow_checkpoints` / `workflow_pending_writes`
143
+ * rows for every thread linked to the session, whether through the
144
+ * workflow-run attachment mapping or through the `sessionId`
145
+ * checkpoint metadata the agent runtime stamps on HITL suspends -
146
+ * those snapshots embed the full conversation. After this call the
147
+ * conversation is no longer retrievable through `memory.session.*`
148
+ * search surfaces nor resumable from its checkpoints. A no-op for an
149
+ * unknown id. Custom implementations must honour the same contract in
150
+ * full - leaving any of these surfaces behind defeats erasure.
151
+ */
152
+ deleteSession(sessionId: string): Promise<void>;
153
+ /**
154
+ * Retention sweep (RP-6): hard-delete (cascade) every session matching the
155
+ * policy. `beforeEpochMs` limits to sessions created before that instant;
156
+ * `closedOnly` limits to closed sessions. With neither, deletes all sessions.
157
+ * Returns the number of sessions deleted.
158
+ */
159
+ pruneSessions(opts: {
160
+ readonly beforeEpochMs?: number;
161
+ readonly closedOnly?: boolean;
162
+ }): Promise<number>;
163
+ }
@@ -0,0 +1,23 @@
1
+ import type { Message } from '../types/message.js';
2
+
3
+ /**
4
+ * Pluggable token counter. Implementations live in `@graphorin/provider`
5
+ * (default `JsTiktokenCounter` for OpenAI/compatible, plus per-vendor
6
+ * native counters) and are interchangeable behind this interface.
7
+ *
8
+ * Counters carry a `version` field so that consumers (e.g. the
9
+ * `session_messages.tokenizer_version` cache column) can invalidate stale
10
+ * cached counts when the underlying tokenizer is upgraded.
11
+ *
12
+ * @stable
13
+ */
14
+ export interface TokenCounter {
15
+ /** Human-readable identifier (`'js-tiktoken@cl100k_base'`, …). */
16
+ readonly id: string;
17
+ /** Tokenizer version string used for cache invalidation. */
18
+ readonly version: string;
19
+ /** Count tokens in a list of `Message`s (system/user/assistant/tool). */
20
+ count(messages: ReadonlyArray<Message>): Promise<number>;
21
+ /** Count tokens in a raw text string. */
22
+ countText(text: string): Promise<number>;
23
+ }
@@ -0,0 +1,397 @@
1
+ import type { MessageContent } from '../types/message.js';
2
+ import type { RunContext } from '../types/run.js';
3
+ import type {
4
+ ContentChunk,
5
+ InboundSanitizationPolicy,
6
+ MemoryGuardTier,
7
+ SandboxPolicy,
8
+ SideEffectClass,
9
+ ToolSource,
10
+ ToolTrustClass,
11
+ TruncationStrategy,
12
+ } from '../types/tool.js';
13
+ import type { ZodLikeSchema } from '../utils/validation.js';
14
+ import type { Logger } from './logger.js';
15
+ import type { ModelHint, ModelSpec } from './preferred-model.js';
16
+ import type { Tracer } from './tracer.js';
17
+
18
+ /**
19
+ * Pluggable function call exposed to an LLM. Concrete `Tool` instances
20
+ * are produced by the `tool({...})` factory in `@graphorin/tools` and
21
+ * by the MCP / Skills loaders. The interface lives in core because every
22
+ * package above the persistence layer (agent runtime, workflow engine,
23
+ * server, sessions, observability, …) carries `Tool[]` references on
24
+ * its public surface.
25
+ *
26
+ * The interface is intentionally minimal - extension fields covered by
27
+ * later phases (per-tool `secretsAllowed` ACL, inbound sanitization
28
+ * policy, result truncation strategy, streaming hint, …) are added
29
+ * **additively** by their owning packages so that v0.1 consumers can
30
+ * type their tool list against `Tool<...>` today without having to
31
+ * worry about future fields.
32
+ *
33
+ * @stable
34
+ */
35
+ export interface Tool<TInput = unknown, TOutput = unknown, TDeps = unknown> {
36
+ readonly name: string;
37
+ readonly description: string;
38
+ readonly inputSchema: ZodLikeSchema<TInput>;
39
+ readonly outputSchema?: ZodLikeSchema<TOutput>;
40
+ /**
41
+ * Either a static boolean or a predicate consulted at runtime against
42
+ * the realized input. `true` means the runtime suspends the run with a
43
+ * `tool.approval.requested` event before the tool executes.
44
+ */
45
+ readonly needsApproval?:
46
+ | boolean
47
+ | ((input: TInput, ctx: ToolExecutionContext<TDeps>) => boolean | Promise<boolean>);
48
+ /**
49
+ * Sandbox isolation level. Defaults are picked by
50
+ * `@graphorin/security`. ADVISORY in the default agent build (AG-18):
51
+ * inline `config.tools` closures cannot be serialised out-of-process,
52
+ * so the resolved policy is surfaced on the `tool.execute` span /
53
+ * audit but the tool runs in-process. Real isolation applies to
54
+ * module-loadable (skill / MCP) tools.
55
+ */
56
+ readonly sandboxPolicy?: SandboxPolicy;
57
+ /** Free-form labels surfaced to operators and to the model. */
58
+ readonly tags?: ReadonlyArray<string>;
59
+ /**
60
+ * Sequential execution mode hints. Tools tagged `'sequential'` are
61
+ * never executed in parallel with each other; the executor serializes
62
+ * them inside the per-step batch.
63
+ *
64
+ * @default `'parallel'`
65
+ */
66
+ readonly executionMode?: 'parallel' | 'sequential';
67
+ /**
68
+ * Per-tool secrets ACL. Tool execution is wrapped in a scope where
69
+ * `ctx.secrets.require(...)` only resolves keys present here.
70
+ * Empty / undefined means the tool may not request any secret.
71
+ */
72
+ readonly secretsAllowed?: ReadonlyArray<string>;
73
+ /**
74
+ * Sensitivity ceiling of the tool's input + output payload. Used by
75
+ * the redaction validator to decide whether the result may flow to a
76
+ * given sink (provider / exporter).
77
+ */
78
+ readonly sensitivity?: import('../types/sensitivity.js').Sensitivity;
79
+ /**
80
+ * Memory-modification guard tier (DEC-153). ACTIVE when the agent is
81
+ * created with `memory` wired (SDF-1): the runtime binds a scope-aware
82
+ * region reader over working memory and the executor snapshots/verifies
83
+ * the region around guarded calls. Without `memory` the guard is
84
+ * skipped and the agent emits a one-time WARN.
85
+ */
86
+ readonly memoryGuardTier?: MemoryGuardTier;
87
+ /** Inbound prompt-injection sanitization policy. */
88
+ readonly inboundSanitization?: InboundSanitizationPolicy;
89
+ /**
90
+ * When `true`, an inbound-sanitization hit returns `ToolError({ kind:
91
+ * 'inbound_sanitization_blocked' })` instead of forwarding the
92
+ * (sanitized) result. Intended for regulated deployments.
93
+ *
94
+ * @default `false`
95
+ */
96
+ readonly failClosed?: boolean;
97
+ /**
98
+ * Defer the tool from the per-step catalogue until the model invokes
99
+ * the built-in `tool_search` to look it up. Tools with deferred
100
+ * loading are not advertised to the model on every step, which keeps
101
+ * the input-token cost bounded for installations with dozens of
102
+ * MCP-derived tools.
103
+ *
104
+ * Naming note (W-127): the snake_case is DELIBERATE - this field
105
+ * mirrors the wire-level `defer_loading` flag of the Anthropic
106
+ * tool-use surface one-to-one, so grep and serialized payloads
107
+ * match. It is the only snake_case field on `Tool` by design, not
108
+ * an oversight.
109
+ *
110
+ * @default `false`
111
+ */
112
+ readonly defer_loading?: boolean;
113
+ /**
114
+ * Maximum number of tokens the assembled tool result may carry into
115
+ * the conversation history. `0` disables the cap (logs a one-time
116
+ * WARN at registration). Counted against text-shaped output and
117
+ * text-shaped `contentParts` entries; non-text parts pass through.
118
+ *
119
+ * @default `16384`
120
+ */
121
+ readonly maxResultTokens?: number;
122
+ /** Truncation strategy applied when `maxResultTokens` is exceeded. */
123
+ readonly truncationStrategy?: TruncationStrategy;
124
+ /**
125
+ * Worked examples shown to the model alongside the tool's
126
+ * description. Bounded `[1, 5]` - overflow emits a one-time WARN at
127
+ * registration. Each example's `input` and `output` is validated
128
+ * against the tool's `inputSchema` / `outputSchema`.
129
+ */
130
+ readonly examples?: ReadonlyArray<ToolExample<TInput, TOutput>>;
131
+ /**
132
+ * Render examples eagerly (every step) regardless of
133
+ * `defer_loading`. When undefined the runtime applies the auto-rule:
134
+ * `defer_loading: true` ⇒ `false`; `defer_loading: false` ⇒ `true`;
135
+ * neither ⇒ `undefined` (the agent runtime decides at assembly time).
136
+ */
137
+ readonly examplesEagerlyRendered?: boolean;
138
+ /**
139
+ * REQUIRED side-effect classification. v0.1 transition mode emits a
140
+ * one-time WARN per tool name on missing classification and applies
141
+ * the conservative deferred default `'side-effecting'`; v0.2 may
142
+ * promote the WARN to a registration error.
143
+ */
144
+ readonly sideEffectClass?: SideEffectClass;
145
+ /**
146
+ * Optional callback returning a deterministic dedup key per
147
+ * `(input, ctx)` tuple. REQUIRED-by-WARN for `'side-effecting'` /
148
+ * `'external-stateful'` tools. The framework does not validate
149
+ * determinism - that is the operator's contract.
150
+ */
151
+ readonly idempotencyKey?: (
152
+ input: TInput,
153
+ ctx: ToolExecutionContext<TDeps>,
154
+ ) => string | Promise<string>;
155
+ /**
156
+ * Opt-in flag for streaming-tool execution. The `?: true` typing
157
+ * rejects `streamingHint: false` on purpose - absence is the
158
+ * canonical "non-streaming" signal preserving v0.1 behaviour. When
159
+ * `true`, `Tool.execute(...)` may call `ctx.streamContent(...)` /
160
+ * `ctx.reportProgress(...)` and may return `Promise<void>`.
161
+ */
162
+ readonly streamingHint?: true;
163
+ /**
164
+ * Per-tool author-time model hint. Either a cost-tier vocabulary
165
+ * literal (`'fast' | 'balanced' | 'smart'`) OR an explicit
166
+ * `ModelSpec` that always wins over the agent-side tier mapping.
167
+ */
168
+ readonly preferredModel?: ModelHint | ModelSpec;
169
+ /**
170
+ * Execute the tool. Concrete implementations may return either a raw
171
+ * `TOutput` or a `ToolReturn<TOutput>` envelope when extra content
172
+ * parts (images, files, …) need to be appended to the conversation.
173
+ * Streaming-hint tools may also return `void` once the per-chunk
174
+ * buffer has been populated.
175
+ */
176
+ execute(
177
+ input: TInput,
178
+ ctx: ToolExecutionContext<TDeps>,
179
+ ): Promise<TOutput | ToolReturn<TOutput> | undefined>;
180
+ }
181
+
182
+ /**
183
+ * Existentially-typed {@link Tool} for collection seams (W-100).
184
+ *
185
+ * `Tool` is invariant in `TInput` (the `needsApproval` /
186
+ * `idempotencyKey` predicate properties are contravariant in it), so a
187
+ * concretely-typed `Tool<{ q: string }, number, D>` is NOT assignable
188
+ * to `Tool<unknown, unknown, D>` - which forced `as unknown as Tool`
189
+ * casts wherever tools are collected. `AnyTool` erases `TInput` /
190
+ * `TOutput` existentially, following the `HandoffEntry` precedent in
191
+ * `@graphorin/agent`.
192
+ *
193
+ * Use it on COLLECTION seams (`createAgent({ tools })`, executor
194
+ * options, registries); implement tools against the typed `Tool` via
195
+ * the `tool({...})` factory.
196
+ *
197
+ * @stable
198
+ */
199
+ // biome-ignore lint/suspicious/noExplicitAny: existential TInput/TOutput (see above)
200
+ export type AnyTool<TDeps = unknown> = Tool<any, any, TDeps>;
201
+
202
+ /**
203
+ * Worked example for a `Tool`. Type-parameterized on the same generics
204
+ * as `Tool`, so a `ToolExample` for `Tool<{ q: string }, { hits: T[] }>`
205
+ * cannot specify an `input` shape that does not match.
206
+ *
207
+ * @stable
208
+ */
209
+ export interface ToolExample<TInput = unknown, TOutput = unknown> {
210
+ readonly input: TInput;
211
+ readonly output: TOutput | ToolReturn<TOutput>;
212
+ readonly comment?: string;
213
+ }
214
+
215
+ /**
216
+ * Resolved record returned by the `ToolRegistry` getter. Carries every
217
+ * non-public registration-time field downstream layers consume
218
+ * (sanitization, audit, retrieval, side-effect classification,
219
+ * collision resolution, …) so consumers do not have to recompute it.
220
+ *
221
+ * @stable
222
+ */
223
+ export interface ResolvedTool<TInput = unknown, TOutput = unknown, TDeps = unknown>
224
+ extends Tool<TInput, TOutput, TDeps> {
225
+ readonly __trustClass: ToolTrustClass;
226
+ readonly __source: ToolSource;
227
+ readonly __effectiveDeferLoading: boolean;
228
+ readonly __sideEffectClass: SideEffectClass;
229
+ readonly __hasIdempotencyKey: boolean;
230
+ readonly __streamingHint: boolean;
231
+ readonly __exampleCount: number;
232
+ readonly __preferredModel?: ModelHint | ModelSpec;
233
+ }
234
+
235
+ /**
236
+ * Optional return envelope: pairs a typed `output` (passed to the model)
237
+ * with extra `contentParts` that are appended verbatim to the
238
+ * conversation (images, files, audio, …).
239
+ *
240
+ * @stable
241
+ */
242
+ export interface ToolReturn<TOutput = unknown> {
243
+ /**
244
+ * W-115: envelope brand set by the {@link toolReturn} factory.
245
+ * `Symbol.for`, so duplicate package copies agree. Prefer branding:
246
+ * the structural fallback in the executor is deliberately narrow
247
+ * (own keys within `{output, contentParts, taint}`), and plain data
248
+ * that happens to be exactly `{ output: X }` is ambiguous by
249
+ * construction - brand it (or rename the field) to disambiguate.
250
+ */
251
+ readonly [TOOL_RETURN_BRAND]?: true;
252
+ readonly output: TOutput;
253
+ readonly contentParts?: ReadonlyArray<MessageContent>;
254
+ /**
255
+ * C6: per-result taint override the data-flow ledger honours when
256
+ * recording this output. Lets a FIRST-PARTY tool whose CONTENT is not
257
+ * first-party (e.g. memory recall returning quarantined /
258
+ * foreign-provenance facts) re-arm the taint ledger, closing the
259
+ * cross-session poisoning leg. Flags only ever WIDEN the derived label
260
+ * (they cannot launder an untrusted tool's output into trusted).
261
+ */
262
+ readonly taint?: {
263
+ readonly untrusted?: boolean;
264
+ readonly sensitive?: boolean;
265
+ readonly sourceKind?: string;
266
+ };
267
+ }
268
+
269
+ /**
270
+ * W-115: cross-realm brand for the {@link ToolReturn} envelope
271
+ * (`SECRET_VALUE_BRAND` precedent - `Symbol.for` survives duplicate
272
+ * package copies).
273
+ *
274
+ * @stable
275
+ */
276
+ export const TOOL_RETURN_BRAND: unique symbol = Symbol.for('graphorin.ToolReturn');
277
+
278
+ /**
279
+ * W-115: build a BRANDED {@link ToolReturn} envelope. The executor
280
+ * unwraps branded envelopes unconditionally; unbranded objects fall to
281
+ * a deliberately narrow structural sniff (own keys within
282
+ * `{output, contentParts, taint}`), so a tool legitimately returning
283
+ * `{ output, exitCode, stderr }` is no longer silently stripped to its
284
+ * `output` field.
285
+ *
286
+ * @stable
287
+ */
288
+ export function toolReturn<TOutput>(fields: {
289
+ readonly output: TOutput;
290
+ readonly contentParts?: ReadonlyArray<MessageContent>;
291
+ readonly taint?: ToolReturn<TOutput>['taint'];
292
+ }): ToolReturn<TOutput> {
293
+ return {
294
+ [TOOL_RETURN_BRAND]: true,
295
+ output: fields.output,
296
+ ...(fields.contentParts !== undefined ? { contentParts: fields.contentParts } : {}),
297
+ ...(fields.taint !== undefined ? { taint: fields.taint } : {}),
298
+ };
299
+ }
300
+
301
+ /**
302
+ * W-115: the ONE guard for the ToolReturn envelope (the executor and
303
+ * the registry example-normalizer both consume it). Brand first; the
304
+ * structural fallback accepts only objects whose OWN enumerable keys
305
+ * all belong to the canonical envelope shape - `{output, exitCode}`
306
+ * style process results pass through intact.
307
+ *
308
+ * @stable
309
+ */
310
+ export function isToolReturnEnvelope<TOutput = unknown>(
311
+ value: unknown,
312
+ ): value is ToolReturn<TOutput> {
313
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
314
+ if ((value as Record<PropertyKey, unknown>)[TOOL_RETURN_BRAND] === true) return true;
315
+ if (!Object.hasOwn(value, 'output')) return false;
316
+ for (const key of Object.keys(value)) {
317
+ if (key !== 'output' && key !== 'contentParts' && key !== 'taint') return false;
318
+ }
319
+ return true;
320
+ }
321
+
322
+ /**
323
+ * W-115: `true` when {@link isToolReturnEnvelope} matched WITHOUT the
324
+ * brand - observability for the future deprecation of the structural
325
+ * sniff.
326
+ *
327
+ * @stable
328
+ */
329
+ export function isUnbrandedToolReturn(value: unknown): boolean {
330
+ return (
331
+ isToolReturnEnvelope(value) &&
332
+ (value as unknown as Record<PropertyKey, unknown>)[TOOL_RETURN_BRAND] !== true
333
+ );
334
+ }
335
+
336
+ /**
337
+ * Per-call execution context handed to `Tool.execute(...)`. Carries the
338
+ * stable `toolCallId`, the parent `RunContext`, an `AbortSignal` tied to
339
+ * the surrounding agent run, structured tracer / logger handles, the
340
+ * streaming progress / content emitters, and a per-call secrets accessor
341
+ * scoped to the tool's `secretsAllowed` ACL.
342
+ *
343
+ * @stable
344
+ */
345
+ export interface ToolExecutionContext<TDeps = unknown> {
346
+ readonly toolCallId: string;
347
+ readonly runContext: RunContext<TDeps>;
348
+ readonly signal: AbortSignal;
349
+ readonly tracer: Tracer;
350
+ readonly logger: Logger;
351
+ /**
352
+ * Per-call secrets accessor. The accessor enforces the tool's
353
+ * `secretsAllowed` ACL - calling `require(...)` for a key that is
354
+ * not on the allowlist throws `SecretAccessDeniedError`.
355
+ */
356
+ readonly secrets: ToolSecretsAccessor;
357
+ /**
358
+ * Emit a progress event to subscribers of `agent.stream(...)`. No-op
359
+ * on tools without `streamingHint: true` AND on aborted streams. The
360
+ * counter pair `(current, total?)` is consumer-rendered as a
361
+ * percentage when both fields are present.
362
+ */
363
+ reportProgress(current: number, total?: number, message?: string): void;
364
+ /**
365
+ * Emit one chunk of content. Concatenated into the tool's assembled
366
+ * `output` per the buffer-becomes-output discipline. No-op on tools
367
+ * without `streamingHint: true` AND on aborted streams.
368
+ */
369
+ streamContent(chunk: ContentChunk): void;
370
+ }
371
+
372
+ /**
373
+ * Per-call secrets accessor surface. Implemented by the executor; the
374
+ * tool author calls `require(...)` to obtain a `SecretValue` wrapper.
375
+ *
376
+ * The accessor is intentionally narrow - the ACL enforcement happens
377
+ * inside `require(...)`, so the tool author never accidentally
378
+ * unwraps a secret outside the tool's permitted set.
379
+ *
380
+ * @stable
381
+ */
382
+ export interface ToolSecretsAccessor {
383
+ /**
384
+ * Resolve a secret by key. Throws `SecretAccessDeniedError` if the
385
+ * key is not in the tool's `secretsAllowed` allowlist; throws
386
+ * `SecretRequiredError` (or returns `null` when `optional: true`)
387
+ * if the key resolves to no value.
388
+ */
389
+ require(
390
+ key: string,
391
+ options?: { readonly optional?: false },
392
+ ): Promise<import('./secret-value.js').SecretValue>;
393
+ require(
394
+ key: string,
395
+ options: { readonly optional: true },
396
+ ): Promise<import('./secret-value.js').SecretValue | null>;
397
+ }