@ziggs-ai/agent-sdk 0.1.3 → 0.1.5

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 (98) hide show
  1. package/README.md +3 -1
  2. package/package.json +9 -4
  3. package/src/AgentHost.ts +495 -0
  4. package/src/adapters/OpenAIAdapter.ts +146 -0
  5. package/src/agent/Agent.ts +101 -0
  6. package/src/cognition/validateContext.ts +95 -0
  7. package/src/context/applyEffects.ts +80 -0
  8. package/src/context/batch.ts +17 -0
  9. package/src/context/classifyEnvelope.ts +38 -0
  10. package/src/context/routingLabels.ts +46 -0
  11. package/src/defineAgent.ts +62 -0
  12. package/src/formatters/AgreementFormatter.ts +111 -0
  13. package/src/formatters/HistoryFormatter.ts +166 -0
  14. package/src/formatters/index.ts +2 -0
  15. package/src/index.ts +105 -0
  16. package/src/ingress/normalizeIncoming.ts +162 -0
  17. package/src/memory/MemoryStore.ts +104 -0
  18. package/src/pricing/fleetDefaults.ts +218 -0
  19. package/src/pricing/fleetEvalFree.ts +24 -0
  20. package/src/pricing/fleetFreeTierA.gen.ts +12 -0
  21. package/src/pricing/fleetTierByAgentId.gen.ts +1022 -0
  22. package/src/runtime/AgentMachine.ts +364 -0
  23. package/src/runtime/PromptBuilder.ts +463 -0
  24. package/src/runtime/buildOutcome.ts +518 -0
  25. package/src/runtime/defaults.ts +75 -0
  26. package/src/runtime/runTurn.ts +691 -0
  27. package/src/runtime/validateWorkflow.ts +181 -0
  28. package/src/server/ConnectionPool.ts +155 -0
  29. package/src/server/EventQueue.ts +133 -0
  30. package/src/server/InboxCatchUp.ts +251 -0
  31. package/src/server/OutboxBuffer.ts +90 -0
  32. package/src/server/SeenMessages.ts +27 -0
  33. package/src/server/ZiggsEffectHandler.ts +409 -0
  34. package/src/server/agreements/AgreementService.ts +117 -0
  35. package/src/server/createHealthServer.ts +85 -0
  36. package/src/server/proactive/ProactiveTrigger.ts +83 -0
  37. package/src/server/runLauncher.ts +146 -0
  38. package/src/server/tasks/TaskService.ts +110 -0
  39. package/src/server/tasks/index.ts +1 -0
  40. package/src/server/telemetryIngest.ts +91 -0
  41. package/src/server/tools/index.ts +46 -0
  42. package/src/server/tools/tier1/protocolRunner.ts +133 -0
  43. package/src/server/tools/tier1/protocolTools.ts +99 -0
  44. package/src/server/tools/tier2/connectionTools.ts +75 -0
  45. package/src/server/tools/tier2/contextTools.ts +74 -0
  46. package/src/server/tools/tier2/discoveryTools.ts +34 -0
  47. package/src/server/tools/tier2/marketplaceTools.ts +25 -0
  48. package/src/server/tools/tier2/paymentTools.ts +193 -0
  49. package/src/server/ziggsconnect/ZiggsConnectClient.ts +126 -0
  50. package/src/server/ziggscontext/ZiggsContextClient.ts +137 -0
  51. package/src/server/ziggspay/ZiggsPayClient.ts +193 -0
  52. package/src/shared/ids.ts +3 -0
  53. package/src/shared/runtimeLog.ts +72 -0
  54. package/src/shared/types.ts +29 -0
  55. package/src/tasks/protocolRegistry.ts +25 -0
  56. package/src/tools/ToolManager.ts +95 -0
  57. package/src/tools/{ToolProvider.js → ToolProvider.ts} +5 -15
  58. package/src/tools/defineTool.ts +90 -0
  59. package/src/tools/index.ts +5 -0
  60. package/src/types.ts +407 -0
  61. package/src/utils/jsonExtractor.ts +100 -0
  62. package/src/ConnectionPool.js +0 -133
  63. package/src/adapters/OpenAIAdapter.js +0 -73
  64. package/src/agent/Agent.js +0 -121
  65. package/src/agent/EventQueue.js +0 -68
  66. package/src/agent/OutboxBuffer.js +0 -62
  67. package/src/cognition/PromptBuilder.js +0 -312
  68. package/src/cognition/resolveActionTool.js +0 -12
  69. package/src/cognition/runTurn.js +0 -578
  70. package/src/context/applyEffects.js +0 -133
  71. package/src/context/batch.js +0 -25
  72. package/src/context/classifyEnvelope.js +0 -82
  73. package/src/context/routingLabels.js +0 -54
  74. package/src/createHealthServer.js +0 -28
  75. package/src/formatters/HistoryFormatter.js +0 -257
  76. package/src/formatters/TaskFormatter.js +0 -180
  77. package/src/formatters/index.js +0 -9
  78. package/src/index.js +0 -76
  79. package/src/ingress/normalizeIncoming.js +0 -70
  80. package/src/runLauncher.js +0 -159
  81. package/src/shared/ids.js +0 -7
  82. package/src/shared/types.js +0 -86
  83. package/src/tasks/TaskService.js +0 -247
  84. package/src/tasks/index.js +0 -9
  85. package/src/tasks/taskCore.js +0 -229
  86. package/src/tasks/taskProtocolRegistry.js +0 -22
  87. package/src/tasks/taskProtocolRunner.js +0 -107
  88. package/src/tasks/taskProtocolTools.js +0 -87
  89. package/src/tools/ToolManager.js +0 -79
  90. package/src/tools/defineTool.js +0 -82
  91. package/src/tools/index.js +0 -11
  92. package/src/utils/jsonExtractor.js +0 -139
  93. package/src/workflow/AgentMachine.js +0 -250
  94. package/src/workflow/WorkflowRuntime.js +0 -63
  95. package/src/workflow/dsl.js +0 -287
  96. package/src/workflow/motifs.js +0 -435
  97. package/src/ziggs/runtime.js +0 -192
  98. /package/src/adapters/{index.js → index.ts} +0 -0
@@ -0,0 +1,193 @@
1
+ import 'dotenv/config';
2
+
3
+ function getDefaultBaseUrl(): string {
4
+ return process.env.BACKEND_URL || process.env.ZIGGS_BACKEND_URL || 'http://localhost:3000';
5
+ }
6
+
7
+ function randomIdempotencyKey(prefix = 'op'): string {
8
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
9
+ }
10
+
11
+ function parseError(text: string, fallback: string): string {
12
+ if (!text) return fallback;
13
+ try {
14
+ const parsed = JSON.parse(text) as Record<string, unknown>;
15
+ return (parsed['error'] as string) || (parsed['message'] as string) || text;
16
+ } catch { return text; }
17
+ }
18
+
19
+ interface PayError extends Error { status?: number; body?: string; }
20
+
21
+ export interface ZiggsPayClientOptions {
22
+ operatorKey: string;
23
+ actAsAgentId?: string;
24
+ baseUrl?: string;
25
+ }
26
+
27
+ export class ZiggsPayClient {
28
+ private operatorKey: string;
29
+ private actAsAgentId: string | null;
30
+ private baseUrl: string;
31
+
32
+ constructor({ operatorKey, actAsAgentId, baseUrl }: ZiggsPayClientOptions) {
33
+ if (!operatorKey || typeof operatorKey !== 'string') throw new Error('ZiggsPayClient requires an operatorKey');
34
+ this.operatorKey = operatorKey;
35
+ this.actAsAgentId = actAsAgentId || null;
36
+ this.baseUrl = baseUrl || getDefaultBaseUrl();
37
+ }
38
+
39
+ async balance(): Promise<{ walletId: string | null; currency: string; balance: number; availableBalance: number }> {
40
+ const w = await this._get('/payments/wallet') as Record<string, unknown>;
41
+ const wallet = w['wallet'] as Record<string, unknown> | undefined;
42
+ return { walletId: (wallet?.['walletId'] as string) || null, currency: (wallet?.['currency'] as string) || 'pez', balance: (w['balance'] as number) ?? 0, availableBalance: (w['availableBalance'] as number) ?? 0 };
43
+ }
44
+
45
+ async resolve({ userId, agentId }: { userId?: string; agentId?: string } = {}): Promise<unknown> {
46
+ if (!userId && !agentId) throw new Error('resolve: provide userId or agentId');
47
+ const params = new URLSearchParams();
48
+ if (userId) params.set('userId', userId);
49
+ if (agentId) params.set('agentId', agentId);
50
+ const res = await this._get(`/payments/wallets/resolve?${params}`) as Record<string, unknown>;
51
+ return res['wallet'] || null;
52
+ }
53
+
54
+ async transfer({ to, amount, idempotencyKey, description, paymentGrantId }: { to: string; amount: number; idempotencyKey?: string; description?: string; paymentGrantId?: string }): Promise<unknown> {
55
+ if (!to) throw new Error('transfer: `to` is required');
56
+ if (!(Number.isInteger(amount) && amount > 0)) throw new Error('transfer: `amount` must be a positive integer (cents)');
57
+ if (this.actAsAgentId && !paymentGrantId) throw new Error('transfer: paymentGrantId is required for agent-impersonated transfers. The wallet owner must have issued a payment grant to this agentId.');
58
+ let toWalletId = to;
59
+ if (!to.startsWith('wal_')) {
60
+ const w = await this.resolve(to.startsWith('agent_') ? { agentId: to } : { userId: to }) as Record<string, unknown> | null;
61
+ if (!w?.['walletId']) throw new Error(`transfer: could not resolve wallet for "${to}"`);
62
+ toWalletId = w['walletId'] as string;
63
+ }
64
+ const result = await this._post('/payments/transfer', { toWalletId, amount, idempotencyKey: idempotencyKey || randomIdempotencyKey('xfer'), description, paymentGrantId }) as Record<string, unknown>;
65
+ if (result?.['status'] === 'approval_required') {
66
+ const approval = (result['approval'] as Record<string, unknown>) || {};
67
+ return { status: 'approval_required', approvalId: approval['approvalId'] || null, expiresAt: approval['expiresAt'] || null, reason: approval['reason'] || 'Amount exceeds auto-approve threshold', toWalletId, amount };
68
+ }
69
+ const tx = result?.['transaction'] as Record<string, unknown> | undefined;
70
+ return { status: 'transferred', transactionId: tx?.['transactionId'] || null, toWalletId, amount };
71
+ }
72
+
73
+ async hold({ amount, idempotencyKey, description }: { amount: number; idempotencyKey?: string; description?: string }): Promise<unknown> {
74
+ if (!(Number.isInteger(amount) && amount > 0)) throw new Error('hold: `amount` must be a positive integer (cents)');
75
+ return this._post('/payments/hold', { amount, idempotencyKey: idempotencyKey || randomIdempotencyKey('hold'), description });
76
+ }
77
+
78
+ async release({ holdId, action = 'complete', toWalletId, idempotencyKey }: { holdId: string; action?: string; toWalletId?: string; idempotencyKey?: string }): Promise<unknown> {
79
+ return this._post(`/payments/release/${holdId}`, { idempotencyKey: idempotencyKey || randomIdempotencyKey('rel'), action, toWalletId });
80
+ }
81
+
82
+ async history(params: { limit?: number; offset?: number; type?: string } = {}): Promise<unknown> {
83
+ const q = new URLSearchParams();
84
+ if (params.limit != null) q.set('limit', String(params.limit));
85
+ if (params.offset != null) q.set('offset', String(params.offset));
86
+ if (params.type) q.set('type', params.type);
87
+ return this._get('/payments/history' + (q.toString() ? `?${q}` : ''));
88
+ }
89
+
90
+ async issueGrant({ holderId, caveats }: { holderId: string; caveats?: unknown }): Promise<unknown> {
91
+ return this._post('/payments/grants', { holderId, caveats });
92
+ }
93
+
94
+ async attenuateGrant({ grantId, holderId, caveats }: { grantId: string; holderId: string; caveats?: unknown }): Promise<unknown> {
95
+ return this._post(`/payments/grants/${grantId}/attenuate`, { holderId, caveats });
96
+ }
97
+
98
+ async revokeGrant(grantId: string): Promise<unknown> {
99
+ return this._request('DELETE', `/payments/grants/${grantId}`, undefined);
100
+ }
101
+
102
+ async listGrants(): Promise<unknown[]> {
103
+ const res = await this._get('/payments/grants') as Record<string, unknown>;
104
+ return (res['grants'] as unknown[]) || [];
105
+ }
106
+
107
+ async createTopUpIntent({ amount, description, currency }: { amount?: number; description?: string; currency?: string } = {}): Promise<unknown> {
108
+ return this._post('/payments/onramp/intent', { amount, description, currency });
109
+ }
110
+
111
+ async confirmMockIntent(intentId: string): Promise<unknown> {
112
+ return this._post(`/payments/onramp/mock-confirm/${intentId}`, {});
113
+ }
114
+
115
+ async faucet({ amount, description = 'Faucet', idempotencyKey }: { amount?: number; description?: string; idempotencyKey?: string } = {}): Promise<unknown> {
116
+ return this._post('/payments/wallet/fund', { amount, idempotencyKey: idempotencyKey || randomIdempotencyKey('faucet'), description });
117
+ }
118
+
119
+ async approvals({ status = 'pending' }: { status?: string } = {}): Promise<unknown[]> {
120
+ const res = await this._get(`/payments/approvals?status=${encodeURIComponent(status)}`) as Record<string, unknown>;
121
+ return (res['approvals'] as unknown[]) || [];
122
+ }
123
+
124
+ async getApproval(approvalId: string): Promise<unknown | null> {
125
+ if (!approvalId) throw new Error('getApproval: approvalId is required');
126
+ try {
127
+ const res = await this._get(`/payments/approvals/${approvalId}`) as Record<string, unknown>;
128
+ return res?.['approval'] || null;
129
+ } catch (err) {
130
+ if ((err as PayError).status === 404) return null;
131
+ throw err;
132
+ }
133
+ }
134
+
135
+ async decide(approvalId: string, decision: string, note = ''): Promise<unknown> {
136
+ return this._post(`/payments/approvals/${approvalId}/decide`, { decision, note });
137
+ }
138
+
139
+ async waitForApproval(approvalId: string, opts: { pollMs?: number; timeoutMs?: number; signal?: AbortSignal } = {}): Promise<unknown> {
140
+ const pollMs = Math.max(500, opts.pollMs ?? 3_000);
141
+ const timeoutMs = Math.max(pollMs, opts.timeoutMs ?? 120_000);
142
+ const signal = opts.signal;
143
+ const deadline = Date.now() + timeoutMs;
144
+
145
+ const sleep = (ms: number) => new Promise<void>((resolve, reject) => {
146
+ const timer = setTimeout(resolve, ms);
147
+ if (signal) {
148
+ if (signal.aborted) { clearTimeout(timer); reject(new Error('aborted')); return; }
149
+ signal.addEventListener('abort', () => { clearTimeout(timer); reject(new Error('aborted')); }, { once: true });
150
+ }
151
+ });
152
+
153
+ while (true) {
154
+ if (signal?.aborted) throw new Error('aborted');
155
+ const approval = await this.getApproval(approvalId) as Record<string, unknown> | null;
156
+ if (!approval) return { status: 'gone' };
157
+ const s = approval['status'];
158
+ if (s === 'executed') return { status: 'executed', approval, transactionId: approval['executedTransactionId'] || null };
159
+ if (s === 'rejected') return { status: 'rejected', approval };
160
+ if (s === 'expired') return { status: 'expired', approval };
161
+ const remaining = deadline - Date.now();
162
+ if (remaining <= 0) return { status: 'timeout', approval };
163
+ await sleep(Math.min(pollMs, remaining));
164
+ }
165
+ }
166
+
167
+ private _buildHeaders(extra: Record<string, string> = {}): Record<string, string> {
168
+ const h: Record<string, string> = { Authorization: `Bearer ${this.operatorKey}`, ...extra };
169
+ if (this.actAsAgentId) h['X-Agent-Id'] = this.actAsAgentId;
170
+ return h;
171
+ }
172
+
173
+ private async _request(method: string, path: string, body: unknown): Promise<unknown> {
174
+ const init: RequestInit = { method, headers: this._buildHeaders(body !== undefined ? { 'content-type': 'application/json' } : {}) };
175
+ if (body !== undefined) init.body = JSON.stringify(body);
176
+ const response = await fetch(`${this.baseUrl}${path}`, init);
177
+ const text = await response.text();
178
+ if (!response.ok) {
179
+ const err = new Error(parseError(text, `HTTP ${response.status}`)) as PayError;
180
+ err.status = response.status;
181
+ err.body = text;
182
+ throw err;
183
+ }
184
+ return text ? JSON.parse(text) : null;
185
+ }
186
+
187
+ private _get(path: string): Promise<unknown> { return this._request('GET', path, undefined); }
188
+ private _post(path: string, body: unknown): Promise<unknown> { return this._request('POST', path, body ?? {}); }
189
+ }
190
+
191
+ export function createZiggsPayClient(opts: ZiggsPayClientOptions): ZiggsPayClient {
192
+ return new ZiggsPayClient(opts);
193
+ }
@@ -0,0 +1,3 @@
1
+ export function generateId(): string {
2
+ return `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
3
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Process-wide log levels for agentplus runtimes. Default is `info` so FSM /
3
+ * socket chatter stays quiet unless operators opt in.
4
+ *
5
+ * Env (first match wins for the debug shortcut):
6
+ * - `DEBUG_AGENTPLUS=1` or `AGENTPLUS_DEBUG=1` → threshold `debug`
7
+ * - `LOG_LEVEL` or `AGENTPLUS_LOG_LEVEL` → `debug` | `info` | `warn` | `error` | `silent`
8
+ * (`silent` keeps only `error` output)
9
+ */
10
+
11
+ export type RuntimeLogLevel = 'debug' | 'info' | 'warn' | 'error';
12
+
13
+ const SEVERITY: Record<RuntimeLogLevel, number> = {
14
+ debug: 0,
15
+ info: 1,
16
+ warn: 2,
17
+ error: 3,
18
+ };
19
+
20
+ function parseThreshold(): number {
21
+ if (process.env.DEBUG_AGENTPLUS === '1' || process.env.AGENTPLUS_DEBUG === '1') {
22
+ return SEVERITY.debug;
23
+ }
24
+ const raw = (
25
+ process.env.LOG_LEVEL ||
26
+ process.env.AGENTPLUS_LOG_LEVEL ||
27
+ 'info'
28
+ ).toLowerCase();
29
+ if (raw === 'silent' || raw === 'none') return SEVERITY.error;
30
+ if (raw === 'debug' || raw === 'trace') return SEVERITY.debug;
31
+ if (raw === 'warn') return SEVERITY.warn;
32
+ if (raw === 'error') return SEVERITY.error;
33
+ return SEVERITY.info;
34
+ }
35
+
36
+ let cachedThreshold: number | null = null;
37
+
38
+ function threshold(): number {
39
+ if (cachedThreshold === null) cachedThreshold = parseThreshold();
40
+ return cachedThreshold;
41
+ }
42
+
43
+ /** Test hook: re-read env after `process.env` mutations. */
44
+ export function resetRuntimeLogLevelCache(): void {
45
+ cachedThreshold = null;
46
+ }
47
+
48
+ function shouldEmit(level: RuntimeLogLevel): boolean {
49
+ return SEVERITY[level] >= threshold();
50
+ }
51
+
52
+ function fmt(scope: string, msg: string): string {
53
+ return `[${scope}] ${msg}`;
54
+ }
55
+
56
+ export const runtimeLog = {
57
+ debug(scope: string, msg: string, ...rest: unknown[]): void {
58
+ if (!shouldEmit('debug')) return;
59
+ console.log(fmt(scope, msg), ...rest);
60
+ },
61
+ info(scope: string, msg: string, ...rest: unknown[]): void {
62
+ if (!shouldEmit('info')) return;
63
+ console.log(fmt(scope, msg), ...rest);
64
+ },
65
+ warn(scope: string, msg: string, ...rest: unknown[]): void {
66
+ if (!shouldEmit('warn')) return;
67
+ console.warn(fmt(scope, msg), ...rest);
68
+ },
69
+ error(scope: string, msg: string, ...rest: unknown[]): void {
70
+ console.error(fmt(scope, msg), ...rest);
71
+ },
72
+ };
@@ -0,0 +1,29 @@
1
+ // Inlined from `@ziggs-ai/api-client` so the pure SDK doesn't depend on the
2
+ // Ziggs HTTP client. Both client and server agree on these string constants
3
+ // because the protocol is the same — they're just two views of one schema.
4
+ export const EntryTypes = {
5
+ MESSAGE: 'message',
6
+ NOTIFICATION: 'notification',
7
+ ARTIFACT: 'artifact',
8
+ TASK_HISTORY: 'task_history',
9
+ } as const;
10
+
11
+ export type EntryType = (typeof EntryTypes)[keyof typeof EntryTypes];
12
+
13
+ export const ContentTypes = {
14
+ TEXT: 'text',
15
+ OPERATION: 'operation',
16
+ CONTEXT: 'context',
17
+ RESULT: 'result',
18
+ TASK_UPDATE: 'task_update',
19
+ THOUGHT: 'thought',
20
+ } as const;
21
+
22
+ export type ContentType = (typeof ContentTypes)[keyof typeof ContentTypes];
23
+
24
+ export const ContextProperties = {
25
+ HISTORY: 'history',
26
+ AGREEMENTS: 'agreements',
27
+ AGENTS: 'agents',
28
+ USERS: 'users',
29
+ } as const;
@@ -0,0 +1,25 @@
1
+ export const PROTOCOL_TOOL_TO_OPERATION = Object.freeze({
2
+ agreement_propose: 'agreement-propose',
3
+ agreement_subcontract: 'agreement-subcontract',
4
+ agreement_respond: 'agreement-respond',
5
+ agreement_counter_proposal: 'agreement-counter-proposal',
6
+ agreement_check_proposal: 'agreement-check-proposal',
7
+ agreement_revoke: 'agreement-revoke',
8
+ task_spawn: 'task-spawn',
9
+ task_update: 'task-update',
10
+ task_update_plan_step: 'task-update-plan-step',
11
+ } as const);
12
+
13
+ export type ProtocolToolName = keyof typeof PROTOCOL_TOOL_TO_OPERATION;
14
+ export type ProtocolOperation = (typeof PROTOCOL_TOOL_TO_OPERATION)[ProtocolToolName];
15
+
16
+ export const PROTOCOL_TOOL_NAMES = Object.freeze(Object.keys(PROTOCOL_TOOL_TO_OPERATION)) as readonly ProtocolToolName[];
17
+
18
+ export function mapProtocolToolToOperation(toolName: string): ProtocolOperation | null {
19
+ if (!toolName || typeof toolName !== 'string') return null;
20
+ return (PROTOCOL_TOOL_TO_OPERATION as Record<string, ProtocolOperation>)[toolName] ?? null;
21
+ }
22
+
23
+ export function isProtocolToolName(toolName: string): toolName is ProtocolToolName {
24
+ return mapProtocolToolToOperation(toolName) != null;
25
+ }
@@ -0,0 +1,95 @@
1
+ import AjvModule from 'ajv';
2
+ import { ToolProvider } from './ToolProvider.js';
3
+ import type { ToolDefinition } from './defineTool.js';
4
+
5
+ // Ajv ships as CJS; handle both ESM default-import shapes
6
+ type ValidateFn = ((data: unknown) => boolean) & { errors?: unknown[] };
7
+ type AjvInstance = { compile: (schema: object) => ValidateFn; errorsText: (errors: unknown) => string };
8
+ const AjvClass = (AjvModule as unknown as { default?: unknown }).default ?? AjvModule;
9
+ const ajv = new (AjvClass as new (opts: object) => AjvInstance)({ allErrors: true, strict: false });
10
+
11
+ interface ToolError extends Error {
12
+ type: string;
13
+ cause?: unknown;
14
+ data?: unknown;
15
+ }
16
+
17
+ function makeError(message: string, type: string, extra?: Partial<ToolError>): ToolError {
18
+ const err = new Error(message) as ToolError;
19
+ err.type = type;
20
+ if (extra) Object.assign(err, extra);
21
+ return err;
22
+ }
23
+
24
+ function stripNullish(args: Record<string, unknown>): Record<string, unknown> {
25
+ if (!args || typeof args !== 'object' || Array.isArray(args)) return args;
26
+ const out: Record<string, unknown> = {};
27
+ for (const [k, v] of Object.entries(args)) {
28
+ if (v !== null && v !== undefined) out[k] = v;
29
+ }
30
+ return out;
31
+ }
32
+
33
+ export class ToolManager extends ToolProvider {
34
+ private tools: Map<string, ToolDefinition>;
35
+
36
+ constructor() {
37
+ super();
38
+ this.tools = new Map();
39
+ }
40
+
41
+ register(tool: ToolDefinition): string {
42
+ if (!tool?.schema) throw makeError('Tool must have a schema', 'invalid_tool');
43
+ if (typeof tool.handler !== 'function') throw makeError('Tool must have a handler function', 'invalid_tool');
44
+ const name = (tool.schema.function?.['name'] || (tool.schema as unknown as Record<string, unknown>)['name']) as string;
45
+ if (!name) throw makeError('Tool schema must have a name', 'invalid_tool');
46
+ this.tools.set(name, tool);
47
+ return name;
48
+ }
49
+
50
+ registerAll(tools: ToolDefinition[] = []): void {
51
+ for (const tool of tools) this.register(tool);
52
+ }
53
+
54
+ async getAvailableTools(): Promise<ToolDefinition[]> {
55
+ return Array.from(this.tools.values());
56
+ }
57
+
58
+ getTool(name: string): ToolDefinition | null {
59
+ const normalized = this._normalizeName(name) as string;
60
+ return this.tools.get(normalized) ?? null;
61
+ }
62
+
63
+ async executeTool(
64
+ name: string,
65
+ args: Record<string, unknown> = {},
66
+ context: Record<string, unknown> = {},
67
+ ): Promise<unknown> {
68
+ const normalized = this._normalizeName(name) as string;
69
+ const tool = this.tools.get(normalized);
70
+ if (!tool) throw makeError(`Tool not found: ${name}`, 'tool_not_found');
71
+
72
+ const cleanedArgs = stripNullish(args);
73
+ this._validateArgs(tool, cleanedArgs);
74
+
75
+ try {
76
+ return await tool.handler(cleanedArgs, context);
77
+ } catch (error) {
78
+ throw makeError(`Tool "${name}" failed: ${(error as Error).message}`, 'tool_error', { cause: error });
79
+ }
80
+ }
81
+
82
+ private _validateArgs(tool: ToolDefinition, args: Record<string, unknown>): void {
83
+ const parameters = tool.schema.function?.['parameters'] || (tool.schema as unknown as Record<string, unknown>)['parameters'];
84
+ if (!parameters) return;
85
+ const validate = ajv.compile(parameters as object);
86
+ if (!validate(args)) {
87
+ const name = tool.schema.function?.['name'];
88
+ throw makeError(
89
+ `Invalid args for "${name}": ${ajv.errorsText(validate.errors)}`,
90
+ 'validation_error',
91
+ { data: { errors: validate.errors } },
92
+ );
93
+ }
94
+ }
95
+ }
@@ -1,28 +1,18 @@
1
- /**
2
- * Base class for tool providers.
3
- * ToolManager and any custom providers extend this.
4
- */
5
1
  export class ToolProvider {
6
- async getAvailableTools() {
2
+ async getAvailableTools(): Promise<unknown[]> {
7
3
  throw new Error('getAvailableTools must be implemented');
8
4
  }
9
-
10
- async executeTool(name, args = {}, context = {}) {
5
+ async executeTool(_name: string, _args?: Record<string, unknown>, _context?: Record<string, unknown>): Promise<unknown> {
11
6
  throw new Error('executeTool must be implemented');
12
7
  }
13
-
14
- getTool(name) {
8
+ getTool(_name: string): unknown | null {
15
9
  throw new Error('getTool must be implemented');
16
10
  }
17
-
18
- _normalizeName(name) {
11
+ _normalizeName(name: unknown): unknown {
19
12
  if (!name || typeof name !== 'string') return name;
20
-
21
13
  const prefixes = ['functions.', 'tools.', 'function.', 'tool.'];
22
14
  for (const prefix of prefixes) {
23
- if (name.startsWith(prefix)) {
24
- return name.substring(prefix.length);
25
- }
15
+ if (name.startsWith(prefix)) return name.substring(prefix.length);
26
16
  }
27
17
  return name;
28
18
  }
@@ -0,0 +1,90 @@
1
+ const PRIMITIVES = new Set(['string', 'number', 'boolean', 'integer']);
2
+
3
+ type FieldDef = string | unknown[] | Record<string, unknown>;
4
+
5
+ function convertField(value: FieldDef): { schema: Record<string, unknown>; required: boolean } {
6
+ if (typeof value === 'string') {
7
+ if (!PRIMITIVES.has(value)) throw new Error(`Unknown type shorthand: "${value}"`);
8
+ return { schema: { type: value }, required: false };
9
+ }
10
+ if (Array.isArray(value)) {
11
+ if (value.length !== 1) throw new Error('Array shorthand must have exactly one element describing the item type');
12
+ const inner = value[0] as FieldDef;
13
+ if (typeof inner === 'string') return { schema: { type: 'array', items: { type: inner } }, required: false };
14
+ return { schema: { type: 'array', items: convertObject(inner as Record<string, unknown>) }, required: false };
15
+ }
16
+ if (typeof value === 'object' && value !== null) {
17
+ const v = value as Record<string, unknown>;
18
+ if (v['type'] && PRIMITIVES.has(v['type'] as string)) {
19
+ const { required: isReq, ...rest } = v;
20
+ return { schema: rest, required: !!isReq };
21
+ }
22
+ if (v['type'] === 'array' || v['type'] === 'object') {
23
+ const { required: isReq, ...rest } = v;
24
+ return { schema: rest, required: !!isReq };
25
+ }
26
+ return { schema: convertObject(v), required: false };
27
+ }
28
+ throw new Error(`Cannot convert field value: ${JSON.stringify(value)}`);
29
+ }
30
+
31
+ function convertObject(fields: Record<string, unknown>): Record<string, unknown> {
32
+ const properties: Record<string, unknown> = {};
33
+ const required: string[] = [];
34
+ for (const [key, value] of Object.entries(fields)) {
35
+ if (key === 'required') continue;
36
+ const { schema, required: isReq } = convertField(value as FieldDef);
37
+ properties[key] = schema;
38
+ if (isReq) required.push(key);
39
+ }
40
+ const result: Record<string, unknown> = { type: 'object', properties };
41
+ if (required.length > 0) result['required'] = required;
42
+ return result;
43
+ }
44
+
45
+ export interface ToolDefinition {
46
+ schema: { type: 'function'; function: Record<string, unknown> };
47
+ handler: (args: Record<string, unknown>, context: Record<string, unknown>) => Promise<unknown>;
48
+ echoUserSummaryOnSuccess?: boolean;
49
+ /**
50
+ * When true, the runtime will skip a second invocation of this tool in
51
+ * the same assistant message. Use for "create an agreement"-style tools
52
+ * where the model is prone to fire two near-identical create calls.
53
+ */
54
+ isAgreementCreation?: boolean;
55
+ /**
56
+ * When true, the runtime will execute this tool even when no state
57
+ * action is bound to it. Use for discovery tools (e.g. agent search,
58
+ * task board) that should always be callable.
59
+ */
60
+ isGenericFallback?: boolean;
61
+ }
62
+
63
+ export interface DefineToolOptions {
64
+ description?: string;
65
+ echoUserSummaryOnSuccess?: boolean;
66
+ isAgreementCreation?: boolean;
67
+ isGenericFallback?: boolean;
68
+ }
69
+
70
+ export function defineTool(
71
+ name: string,
72
+ params: Record<string, unknown>,
73
+ handler: (args: Record<string, unknown>, context: Record<string, unknown>) => Promise<unknown>,
74
+ options: DefineToolOptions = {},
75
+ ): ToolDefinition {
76
+ if (!name || typeof name !== 'string') throw new Error('defineTool: name is required');
77
+ if (!handler || typeof handler !== 'function') throw new Error('defineTool: handler function is required');
78
+
79
+ const parameters = convertObject(params || {});
80
+ const fn: Record<string, unknown> = { name, parameters };
81
+ if (options.description) fn['description'] = options.description;
82
+
83
+ return {
84
+ schema: { type: 'function', function: fn },
85
+ handler,
86
+ ...(options.echoUserSummaryOnSuccess ? { echoUserSummaryOnSuccess: true } : {}),
87
+ ...(options.isAgreementCreation ? { isAgreementCreation: true } : {}),
88
+ ...(options.isGenericFallback ? { isGenericFallback: true } : {}),
89
+ };
90
+ }
@@ -0,0 +1,5 @@
1
+ export { ToolManager } from './ToolManager.js';
2
+ export { ToolProvider } from './ToolProvider.js';
3
+ export { defineTool } from './defineTool.js';
4
+ // Pure protocol-tool name registry (no handlers, no services).
5
+ export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from '../tasks/protocolRegistry.js';