@ziggs-ai/agent-sdk 0.1.3 → 0.1.4

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 (85) hide show
  1. package/README.md +1 -1
  2. package/package.json +9 -4
  3. package/src/AgentHost.ts +342 -0
  4. package/src/adapters/OpenAIAdapter.ts +125 -0
  5. package/src/agent/Agent.ts +98 -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 +86 -0
  16. package/src/ingress/normalizeIncoming.ts +119 -0
  17. package/src/memory/MemoryStore.ts +104 -0
  18. package/src/runtime/AgentMachine.ts +298 -0
  19. package/src/runtime/PromptBuilder.ts +461 -0
  20. package/src/runtime/buildOutcome.ts +488 -0
  21. package/src/runtime/defaults.ts +72 -0
  22. package/src/runtime/runTurn.ts +637 -0
  23. package/src/runtime/validateWorkflow.ts +165 -0
  24. package/src/server/ConnectionPool.ts +155 -0
  25. package/src/server/EventQueue.ts +119 -0
  26. package/src/server/OutboxBuffer.ts +90 -0
  27. package/src/server/ZiggsEffectHandler.ts +335 -0
  28. package/src/server/agreements/AgreementService.ts +111 -0
  29. package/src/server/createHealthServer.ts +8 -0
  30. package/src/server/proactive/ProactiveTrigger.ts +83 -0
  31. package/src/server/runLauncher.ts +131 -0
  32. package/src/server/tasks/TaskService.ts +111 -0
  33. package/src/server/tasks/index.ts +4 -0
  34. package/src/server/tasks/paymentTools.ts +156 -0
  35. package/src/server/tasks/protocolRunner.ts +101 -0
  36. package/src/server/tasks/protocolTools.ts +96 -0
  37. package/src/server/ziggspay/ZiggsPayClient.ts +193 -0
  38. package/src/shared/ids.ts +3 -0
  39. package/src/shared/runtimeLog.ts +72 -0
  40. package/src/shared/types.ts +31 -0
  41. package/src/tasks/protocolRegistry.ts +25 -0
  42. package/src/tasks/taskCore.ts +139 -0
  43. package/src/tools/ToolManager.ts +95 -0
  44. package/src/tools/{ToolProvider.js → ToolProvider.ts} +5 -15
  45. package/src/tools/defineTool.ts +90 -0
  46. package/src/tools/index.ts +5 -0
  47. package/src/types.ts +368 -0
  48. package/src/utils/jsonExtractor.ts +100 -0
  49. package/src/ConnectionPool.js +0 -133
  50. package/src/adapters/OpenAIAdapter.js +0 -73
  51. package/src/agent/Agent.js +0 -121
  52. package/src/agent/EventQueue.js +0 -68
  53. package/src/agent/OutboxBuffer.js +0 -62
  54. package/src/cognition/PromptBuilder.js +0 -312
  55. package/src/cognition/resolveActionTool.js +0 -12
  56. package/src/cognition/runTurn.js +0 -578
  57. package/src/context/applyEffects.js +0 -133
  58. package/src/context/batch.js +0 -25
  59. package/src/context/classifyEnvelope.js +0 -82
  60. package/src/context/routingLabels.js +0 -54
  61. package/src/createHealthServer.js +0 -28
  62. package/src/formatters/HistoryFormatter.js +0 -257
  63. package/src/formatters/TaskFormatter.js +0 -180
  64. package/src/formatters/index.js +0 -9
  65. package/src/index.js +0 -76
  66. package/src/ingress/normalizeIncoming.js +0 -70
  67. package/src/runLauncher.js +0 -159
  68. package/src/shared/ids.js +0 -7
  69. package/src/shared/types.js +0 -86
  70. package/src/tasks/TaskService.js +0 -247
  71. package/src/tasks/index.js +0 -9
  72. package/src/tasks/taskCore.js +0 -229
  73. package/src/tasks/taskProtocolRegistry.js +0 -22
  74. package/src/tasks/taskProtocolRunner.js +0 -107
  75. package/src/tasks/taskProtocolTools.js +0 -87
  76. package/src/tools/ToolManager.js +0 -79
  77. package/src/tools/defineTool.js +0 -82
  78. package/src/tools/index.js +0 -11
  79. package/src/utils/jsonExtractor.js +0 -139
  80. package/src/workflow/AgentMachine.js +0 -250
  81. package/src/workflow/WorkflowRuntime.js +0 -63
  82. package/src/workflow/dsl.js +0 -287
  83. package/src/workflow/motifs.js +0 -435
  84. package/src/ziggs/runtime.js +0 -192
  85. /package/src/adapters/{index.js → index.ts} +0 -0
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Ziggs-flavored EffectHandler.
3
+ *
4
+ * Translates the Agent's pure Effect requests into concrete calls against
5
+ * Ziggs backend primitives (MessagesClient, ArtifactsClient, ScopeClient,
6
+ * taskService, agreementService, messageSender) and the OpenAI adapter. This
7
+ * is the only place where Agent intent meets backend reality.
8
+ *
9
+ * Lives in agent-sdk for now alongside `Agent`; destined for a separate
10
+ * `@ziggs-ai/agent-server` package once the SDK split is complete.
11
+ */
12
+ import type {
13
+ Effect, EffectHandler,
14
+ ContextSnapshot, LlmMessage, LlmResponse, LlmToolSchema, ToolCallResult,
15
+ } from '../types.js';
16
+ import { OutboxBuffer } from './OutboxBuffer.js';
17
+ import { runtimeLog } from '../shared/runtimeLog.js';
18
+ /** Minimal LLM adapter contract — OpenAIAdapter satisfies this. */
19
+ interface LlmAdapter {
20
+ chatMessages(messages: LlmMessage[], tools?: LlmToolSchema[], options?: Record<string, unknown>): Promise<LlmResponse>;
21
+ }
22
+ import type { TaskService } from './tasks/TaskService.js';
23
+ import type { AgreementService } from './agreements/AgreementService.js';
24
+ import type { ToolManager } from '../tools/ToolManager.js';
25
+ import type {
26
+ MessagesClient,
27
+ ArtifactsClient,
28
+ ScopeClient,
29
+ TelemetryClient,
30
+ } from '@ziggs-ai/api-client';
31
+
32
+ export interface ZiggsEffectDeps {
33
+ llm: LlmAdapter;
34
+ messagesClient: MessagesClient;
35
+ artifactsClient: ArtifactsClient;
36
+ scopeClient: ScopeClient;
37
+ taskService: TaskService;
38
+ agreementService: AgreementService;
39
+ messageSender: (text: string, receiverId: string, sessionId: string) => Promise<unknown>;
40
+ toolManager: ToolManager;
41
+ operatorKey: string;
42
+ agentId: string;
43
+ telemetryClient?: TelemetryClient;
44
+ }
45
+
46
+ const CONTEXT_CACHE_MAX = 256;
47
+
48
+ /**
49
+ * Compose a `ContextSnapshot` for a chatId by fanning out to scope + messages
50
+ * primitives. Replaces the single `/context/read` aggregator with parallel
51
+ * pulls. The shape stays the same so PromptBuilder / cognition code doesn't
52
+ * change.
53
+ */
54
+ const annotateAgreement = (ag: Record<string, unknown>, agentId: string): Record<string, unknown> => {
55
+ const parties = (ag?.parties ?? {}) as { creatorId?: string; providerId?: string; payerId?: string; proposedToId?: string };
56
+ return {
57
+ ...ag,
58
+ creatorIsYou: parties.creatorId === agentId,
59
+ providerIsYou: parties.providerId === agentId,
60
+ payerIsYou: parties.payerId === agentId,
61
+ proposedToIsYou: parties.proposedToId === agentId,
62
+ };
63
+ };
64
+
65
+ async function buildSnapshot(
66
+ deps: ZiggsEffectDeps,
67
+ chatId: string,
68
+ agentId: string,
69
+ opts: { after?: string; limit?: number } = {},
70
+ ): Promise<ContextSnapshot> {
71
+ const [scope, msgs, chatAgreements] = await Promise.all([
72
+ deps.scopeClient.get('chat', chatId).catch((err: unknown) => {
73
+ runtimeLog.warn('ZiggsEffectHandler', `scope fetch failed: ${(err as Error)?.message ?? err}`);
74
+ return null;
75
+ }),
76
+ deps.messagesClient
77
+ .list(chatId, { after: opts.after, limit: opts.limit })
78
+ .catch((err: unknown) => {
79
+ runtimeLog.warn('ZiggsEffectHandler', `messages fetch failed: ${(err as Error)?.message ?? err}`);
80
+ return { messages: [], latestSequence: null };
81
+ }),
82
+ deps.agreementService.getByChat(chatId).catch((err: unknown) => {
83
+ runtimeLog.warn('ZiggsEffectHandler', `agreements fetch failed: ${(err as Error)?.message ?? err}`);
84
+ return [] as unknown[];
85
+ }),
86
+ ]);
87
+
88
+ const counterparties = (scope?.accessible?.counterparties ?? []) as Array<{
89
+ id: string;
90
+ role: string;
91
+ principalType?: 'user' | 'agent';
92
+ }>;
93
+
94
+ const others = counterparties.filter((c) => c.id !== agentId);
95
+ const users = others
96
+ .filter((c) => c.principalType === 'user')
97
+ .map((c) => ({ userId: c.id, role: c.role }));
98
+ const agents = others
99
+ .filter((c) => c.principalType !== 'user') // 'agent' or missing → treat as agent
100
+ .map((c) => ({ agentId: c.id, role: c.role, isYou: false }));
101
+ agents.push({ agentId, role: 'self', isYou: true });
102
+
103
+ const agreements = (chatAgreements as Record<string, unknown>[]).map((ag) => annotateAgreement(ag, agentId));
104
+
105
+ return {
106
+ history: msgs.messages as Record<string, unknown>[],
107
+ agreements,
108
+ agents,
109
+ users,
110
+ latestSequence: msgs.latestSequence,
111
+ };
112
+ }
113
+
114
+ const OP_ARTIFACT_MAX_CHARS = 900;
115
+
116
+ function clipForOperationArtifact(value: unknown): unknown {
117
+ if (value == null) return value;
118
+ if (typeof value === 'string') {
119
+ return value.length > OP_ARTIFACT_MAX_CHARS ? `${value.slice(0, OP_ARTIFACT_MAX_CHARS - 1)}…` : value;
120
+ }
121
+ try {
122
+ const s = JSON.stringify(value);
123
+ if (s.length <= OP_ARTIFACT_MAX_CHARS) return value;
124
+ return `${s.slice(0, OP_ARTIFACT_MAX_CHARS - 1)}…`;
125
+ } catch {
126
+ return String(value).slice(0, OP_ARTIFACT_MAX_CHARS);
127
+ }
128
+ }
129
+
130
+ function previewToolArgs(args: Record<string, unknown>): Record<string, unknown> | undefined {
131
+ const keys = Object.keys(args);
132
+ if (keys.length === 0) return undefined;
133
+ const out: Record<string, unknown> = {};
134
+ for (let i = 0; i < keys.length && i < 16; i++) {
135
+ const k = keys[i];
136
+ out[k] = clipForOperationArtifact(args[k]);
137
+ }
138
+ return out;
139
+ }
140
+
141
+ /**
142
+ * Chat timeline: `Message.tsx` parses `entryType: artifact`, `content_type: operation` with
143
+ * `operation_*:{json}` where json includes `state` and `tool`.
144
+ */
145
+ async function emitToolOperationArtifact(
146
+ client: ArtifactsClient,
147
+ chatId: string,
148
+ phase: 'started' | 'completed' | 'error',
149
+ payload: Record<string, unknown>,
150
+ ): Promise<void> {
151
+ const prefix =
152
+ phase === 'started' ? 'operation_started' : phase === 'completed' ? 'operation_completed' : 'operation_error';
153
+ const text = `${prefix}:${JSON.stringify(payload)}`;
154
+ try {
155
+ await client.write({
156
+ chatId,
157
+ text,
158
+ content_type: 'operation',
159
+ visibility: 'agent-private',
160
+ });
161
+ } catch (e: unknown) {
162
+ runtimeLog.warn(
163
+ 'ZiggsEffectHandler',
164
+ `tool operation artifact (${phase}): ${(e as Error)?.message ?? String(e)}`,
165
+ );
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Build a single shared EffectHandler. The handler is stateful — it caches
171
+ * the latest ContextSnapshot per sessionId so tool-call effects can resolve
172
+ * session-coupled deps (the `agents` roster) without forcing the Agent to
173
+ * pass them through. Cache is bounded with simple LRU eviction (re-set on
174
+ * read promotes the entry; oldest entry drops when size exceeds the cap).
175
+ */
176
+ export function createZiggsEffectHandler(deps: ZiggsEffectDeps): EffectHandler {
177
+ const contextCache = new Map<string, ContextSnapshot>();
178
+ const outbox = new OutboxBuffer();
179
+
180
+ const cachePut = (sessionId: string, snap: ContextSnapshot): void => {
181
+ if (contextCache.has(sessionId)) contextCache.delete(sessionId);
182
+ contextCache.set(sessionId, snap);
183
+ if (contextCache.size > CONTEXT_CACHE_MAX) {
184
+ const oldest = contextCache.keys().next().value;
185
+ if (oldest !== undefined) contextCache.delete(oldest);
186
+ }
187
+ };
188
+ const cacheGet = (sessionId: string): ContextSnapshot | undefined => {
189
+ const snap = contextCache.get(sessionId);
190
+ if (snap !== undefined) {
191
+ contextCache.delete(sessionId);
192
+ contextCache.set(sessionId, snap);
193
+ }
194
+ return snap;
195
+ };
196
+
197
+ const handler = async (effect: Effect): Promise<unknown> => {
198
+ switch (effect.kind) {
199
+ case 'read-context': {
200
+ // Composite: assembles ContextSnapshot from scope + messages.
201
+ // Each agent runtime still gets the same shape, but the backend has
202
+ // no `/context/read` — assembly happens here, where the agent's
203
+ // identity is in scope.
204
+ const snap = await buildSnapshot(deps, effect.sessionId, deps.agentId, {
205
+ limit: effect.opts?.maxMessages,
206
+ });
207
+ const me = (snap.agents as Array<{ isYou?: boolean; agentId?: string }>).find((a) => a?.isYou)?.agentId || null;
208
+ outbox.merge(effect.sessionId, snap.history as Record<string, unknown>[], me);
209
+ cachePut(effect.sessionId, snap);
210
+ return snap;
211
+ }
212
+
213
+ case 'list-messages': {
214
+ return deps.messagesClient.list(effect.chatId, {
215
+ after: effect.after,
216
+ limit: effect.limit,
217
+ });
218
+ }
219
+
220
+ case 'list-artifacts': {
221
+ return deps.artifactsClient.list(
222
+ { chatId: effect.chatId, agreementId: effect.agreementId },
223
+ { after: effect.after, limit: effect.limit },
224
+ );
225
+ }
226
+
227
+ case 'list-task-history': {
228
+ const url = new URL(
229
+ `${process.env.ZIGGS_BACKEND_URL ?? 'http://localhost:3000'}/tasks/${effect.taskId}/history`,
230
+ );
231
+ if (effect.after) url.searchParams.set('after', effect.after);
232
+ if (effect.limit != null) url.searchParams.set('limit', String(effect.limit));
233
+ const res = await fetch(url.toString(), {
234
+ headers: {
235
+ 'content-type': 'application/json',
236
+ Authorization: `Bearer ${deps.operatorKey}`,
237
+ 'X-Agent-Id': deps.agentId,
238
+ },
239
+ });
240
+ if (!res.ok) {
241
+ throw new Error(`list-task-history ${res.status} ${res.statusText}`);
242
+ }
243
+ return res.json();
244
+ }
245
+
246
+ case 'get-scope': {
247
+ return deps.scopeClient.get(effect.via.kind, effect.via.id);
248
+ }
249
+
250
+ case 'llm-call': {
251
+ const r: LlmResponse = await deps.llm.chatMessages(effect.messages, effect.tools);
252
+ return r;
253
+ }
254
+
255
+ case 'tool-call': {
256
+ const cached = cacheGet(effect.sessionId);
257
+ const agents = (cached?.agents as unknown[]) || [];
258
+ const users = (cached?.users as unknown[]) || [];
259
+ const args = (effect.args as Record<string, unknown>) || {};
260
+ const argsPreview = previewToolArgs(args);
261
+ await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'started', {
262
+ state: 'started',
263
+ tool: effect.name,
264
+ ...(argsPreview ? { args: argsPreview } : {}),
265
+ });
266
+ try {
267
+ const result = await deps.toolManager.executeTool(
268
+ effect.name,
269
+ args,
270
+ {
271
+ operatorKey: deps.operatorKey,
272
+ agentId: deps.agentId,
273
+ chatId: effect.sessionId,
274
+ taskService: deps.taskService,
275
+ agreementService: deps.agreementService,
276
+ sendMessage: deps.messageSender,
277
+ agents,
278
+ users,
279
+ memory: effect.memory,
280
+ turnScratch: {},
281
+ },
282
+ );
283
+ await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'completed', {
284
+ state: 'completed',
285
+ tool: effect.name,
286
+ result: clipForOperationArtifact(result),
287
+ });
288
+ return { ok: true, result } satisfies ToolCallResult;
289
+ } catch (error: unknown) {
290
+ const errMsg = (error as Error)?.message ?? String(error);
291
+ runtimeLog.warn('ZiggsEffectHandler', `tool-call "${effect.name}" failed: ${errMsg}`);
292
+ await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'error', {
293
+ state: 'failed',
294
+ tool: effect.name,
295
+ error: errMsg,
296
+ });
297
+ return { ok: false, error: errMsg } satisfies ToolCallResult;
298
+ }
299
+ }
300
+
301
+ case 'send-message': {
302
+ await deps.messageSender(effect.text, effect.receiverId, effect.sessionId);
303
+ outbox.track(effect.sessionId, { text: effect.text, receiverId: effect.receiverId });
304
+ return { ok: true };
305
+ }
306
+
307
+ case 'record-event': {
308
+ // Routes through ArtifactsClient as an agent-private artifact —
309
+ // visible to this agent across turns, not to other chat parties.
310
+ try {
311
+ if (effect.entry.kind === 'thought' && typeof effect.entry.text === 'string') {
312
+ await deps.artifactsClient.recordThought(effect.sessionId, effect.entry.text);
313
+ } else {
314
+ await deps.artifactsClient.write({
315
+ chatId: effect.sessionId,
316
+ text: JSON.stringify(effect.entry),
317
+ content_type: String(effect.entry.kind ?? 'event'),
318
+ visibility: 'agent-private',
319
+ });
320
+ }
321
+ } catch (err: unknown) {
322
+ runtimeLog.warn('ZiggsEffectHandler', `record-event failed: ${(err as Error)?.message ?? String(err)}`);
323
+ }
324
+ return { ok: true };
325
+ }
326
+
327
+ default: {
328
+ const exhaustive: never = effect;
329
+ throw new Error(`ZiggsEffectHandler: unknown effect kind ${(exhaustive as Record<string, unknown>).kind}`);
330
+ }
331
+ }
332
+ };
333
+
334
+ return handler as EffectHandler;
335
+ }
@@ -0,0 +1,111 @@
1
+ import {
2
+ proposeAgreement, delegateAgreement, respondToAgreement, counterAgreement,
3
+ getAgreementStatus, listAgreements, getMyAgreements, getAgreement,
4
+ createAgreement, revokeAgreement, getAgreementsByChat, linkAgreementToChat,
5
+ getChatsForAgreement, joinAgreement, linkArtifactToAgreement, getArtifactsForAgreement,
6
+ linkUserToAgreement, getUsersForAgreement, createContract,
7
+ OPEN_AGREEMENT_TARGET,
8
+ type Creds, type Agreement,
9
+ type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput,
10
+ type DelegateAgreementData, type CounterAgreementData,
11
+ type CreateAgreementBody, type ListAgreementsFilters, type GetMyAgreementsFilters,
12
+ type ChatLinkType, type ArtifactLinkType, type UserRole, type CreateContractInput,
13
+ } from '@ziggs-ai/api-client';
14
+
15
+ export class AgreementService {
16
+ private creds: Creds;
17
+
18
+ constructor(operatorKey: string, agentId: string) {
19
+ if (!operatorKey) throw new Error('AgreementService: operatorKey is required');
20
+ if (!agentId) throw new Error('AgreementService: agentId is required (operator-token impersonation)');
21
+ this.creds = { operatorKey, agentId };
22
+ }
23
+
24
+ /** @deprecated Prefer {@link proposeDirect} or {@link proposeBroadcast}. */
25
+ contract(input: CreateContractInput): Promise<Agreement> {
26
+ return createContract(input, this.creds);
27
+ }
28
+
29
+ /** 1:1 proposal. Server defaults engagementKind to `service` when omitted. */
30
+ proposeDirect(payload: ProposeDirectInput): Promise<Agreement> {
31
+ return proposeAgreement(payload, this.creds);
32
+ }
33
+
34
+ /** Open buyer-broadcast (`proposedTo: "everyone"`). Server defaults engagementKind to `service`. */
35
+ proposeBroadcast(payload: ProposeBroadcastInput): Promise<Agreement> {
36
+ return proposeAgreement({ ...payload, proposedTo: OPEN_AGREEMENT_TARGET }, this.creds);
37
+ }
38
+
39
+ /** @deprecated Alias for {@link proposeDirect}. */
40
+ propose(payload: ProposeAgreementData): Promise<Agreement> {
41
+ return proposeAgreement(payload, this.creds);
42
+ }
43
+
44
+ delegate(payload: DelegateAgreementData): Promise<Agreement> {
45
+ return delegateAgreement(payload, this.creds);
46
+ }
47
+
48
+ respond(agreementId: string, action: 'approve' | 'reject'): Promise<Agreement> {
49
+ return respondToAgreement(agreementId, action, this.creds);
50
+ }
51
+
52
+ counter(agreementId: string, counter: CounterAgreementData): Promise<Agreement> {
53
+ return counterAgreement(agreementId, counter || {}, this.creds);
54
+ }
55
+
56
+ getStatus(agreementId: string): Promise<unknown | null> {
57
+ return getAgreementStatus(agreementId, this.creds);
58
+ }
59
+
60
+ list(filters: ListAgreementsFilters = {}): Promise<Agreement[]> {
61
+ return listAgreements(filters, this.creds);
62
+ }
63
+
64
+ mine(filters: GetMyAgreementsFilters = {}): Promise<Agreement[]> {
65
+ return getMyAgreements(filters, this.creds);
66
+ }
67
+
68
+ get(agreementId: string): Promise<Agreement | null> {
69
+ return getAgreement(agreementId, this.creds);
70
+ }
71
+
72
+ create(body: CreateAgreementBody): Promise<{ ok: boolean; agreement: Agreement }> {
73
+ return createAgreement(body, this.creds);
74
+ }
75
+
76
+ revoke(agreementId: string): Promise<{ ok: boolean; agreement: Agreement }> {
77
+ return revokeAgreement(agreementId, this.creds);
78
+ }
79
+
80
+ getByChat(chatId: string): Promise<unknown[]> {
81
+ return getAgreementsByChat(chatId, this.creds);
82
+ }
83
+
84
+ linkToChat(agreementId: string, chatId: string, linkType: ChatLinkType = 'mention'): Promise<unknown | null> {
85
+ return linkAgreementToChat(agreementId, chatId, linkType, this.creds);
86
+ }
87
+
88
+ getChats(agreementId: string): Promise<unknown[]> {
89
+ return getChatsForAgreement(agreementId, this.creds);
90
+ }
91
+
92
+ join(agreementId: string): Promise<{ chatId: string; agentId: string | null; isNew: boolean }> {
93
+ return joinAgreement(agreementId, this.creds);
94
+ }
95
+
96
+ linkArtifact(agreementId: string, artifactId: string, linkType: ArtifactLinkType = 'produced'): Promise<unknown | null> {
97
+ return linkArtifactToAgreement(agreementId, artifactId, linkType, this.creds);
98
+ }
99
+
100
+ getArtifacts(agreementId: string): Promise<unknown[]> {
101
+ return getArtifactsForAgreement(agreementId, this.creds);
102
+ }
103
+
104
+ linkUser(agreementId: string, userId: string, role: UserRole): Promise<unknown | null> {
105
+ return linkUserToAgreement(agreementId, userId, role, this.creds);
106
+ }
107
+
108
+ getUsers(agreementId: string): Promise<unknown[]> {
109
+ return getUsersForAgreement(agreementId, this.creds);
110
+ }
111
+ }
@@ -0,0 +1,8 @@
1
+ import { createServer, type Server } from 'http';
2
+ import { runtimeLog } from '../shared/runtimeLog.js';
3
+
4
+ export function createHealthServer({ port = Number(process.env.PORT) || 8080, label = 'agents' } = {}): Server {
5
+ const server = createServer((_req, res) => { res.writeHead(200); res.end('ok'); });
6
+ server.listen(port, () => runtimeLog.info(label, `health server on port ${port}`));
7
+ return server;
8
+ }
@@ -0,0 +1,83 @@
1
+ import { publishOffer, pullOffers, claimOffer, type Creds } from '@ziggs-ai/api-client';
2
+ import type { PublishOfferPayload, PullOffersOptions } from '@ziggs-ai/api-client';
3
+ import { runtimeLog } from '../../shared/runtimeLog.js';
4
+
5
+ interface TickContext {
6
+ agentId: string;
7
+ marketplace: {
8
+ publishOffer: (payload: PublishOfferPayload) => Promise<unknown>;
9
+ pullOffers: (options?: PullOffersOptions) => Promise<unknown[]>;
10
+ claimOffer: (agreementId: string) => Promise<unknown>;
11
+ };
12
+ }
13
+
14
+ interface ProactiveTriggerOptions {
15
+ interval: number;
16
+ creds: Creds;
17
+ onTick: (ctx: TickContext) => Promise<void>;
18
+ onError?: (err: unknown) => void;
19
+ runImmediately?: boolean;
20
+ }
21
+
22
+ export class ProactiveTrigger {
23
+ private interval: number;
24
+ private creds: Creds;
25
+ private onTick: (ctx: TickContext) => Promise<void>;
26
+ private onError: (err: unknown) => void;
27
+ private runImmediately: boolean;
28
+ private _running: boolean;
29
+ private _timer: ReturnType<typeof setTimeout> | null;
30
+
31
+ constructor({ interval, creds, onTick, onError, runImmediately = false }: ProactiveTriggerOptions) {
32
+ if (!interval || typeof interval !== 'number' || interval < 1000) throw new Error('ProactiveTrigger: interval must be a number >= 1000ms');
33
+ if (!creds?.operatorKey || !creds?.agentId) throw new Error('ProactiveTrigger: creds.operatorKey and creds.agentId are required');
34
+ if (typeof onTick !== 'function') throw new Error('ProactiveTrigger: onTick must be a function');
35
+ this.interval = interval;
36
+ this.creds = creds;
37
+ this.onTick = onTick;
38
+ this.onError = onError ?? ((err) => runtimeLog.error('ProactiveTrigger', `tick error: ${String(err)}`));
39
+ this.runImmediately = runImmediately;
40
+ this._running = false;
41
+ this._timer = null;
42
+ }
43
+
44
+ start(): void {
45
+ if (this._running) return;
46
+ this._running = true;
47
+ runtimeLog.debug(
48
+ 'ProactiveTrigger',
49
+ `started (agentId=${this.creds.agentId}, interval=${this.interval}ms)`,
50
+ );
51
+ if (this.runImmediately) this._tick();
52
+ else this._schedule();
53
+ }
54
+
55
+ stop(): void {
56
+ this._running = false;
57
+ if (this._timer) { clearTimeout(this._timer); this._timer = null; }
58
+ runtimeLog.debug('ProactiveTrigger', `stopped (agentId=${this.creds.agentId})`);
59
+ }
60
+
61
+ private _schedule(): void {
62
+ if (!this._running) return;
63
+ this._timer = setTimeout(() => this._tick(), this.interval);
64
+ }
65
+
66
+ private async _tick(): Promise<void> {
67
+ if (!this._running) return;
68
+ try { await this.onTick(this._buildContext()); } catch (err) { this.onError(err); }
69
+ this._schedule();
70
+ }
71
+
72
+ private _buildContext(): TickContext {
73
+ const creds = this.creds;
74
+ return {
75
+ agentId: creds.agentId,
76
+ marketplace: {
77
+ publishOffer: (payload) => publishOffer(payload, creds),
78
+ pullOffers: (options) => pullOffers(options, creds),
79
+ claimOffer: (agreementId) => claimOffer(agreementId, creds),
80
+ },
81
+ };
82
+ }
83
+ }
@@ -0,0 +1,131 @@
1
+ import { ConnectionPool } from './ConnectionPool.js';
2
+ import { AgentHost, type AgentHostOptions } from '../AgentHost.js';
3
+ import { createHealthServer } from './createHealthServer.js';
4
+ import { runtimeLog } from '../shared/runtimeLog.js';
5
+ import type { Server } from 'http';
6
+
7
+ type AgentConfig = AgentHostOptions & {
8
+ ownAgentId?: string;
9
+ options?: { ownAgentId?: string; name?: string };
10
+ };
11
+
12
+ interface SingleOptions {
13
+ operatorKey?: string;
14
+ label?: string;
15
+ port?: number | string;
16
+ healthServer?: boolean;
17
+ onShutdown?: () => Promise<void>;
18
+ }
19
+
20
+ interface FleetOptions extends SingleOptions {
21
+ wsUrl?: string;
22
+ agentMeta?: Record<string, unknown>[];
23
+ orchestrator?: AgentConfig | ((pool: ConnectionPool) => AgentConfig);
24
+ preWake?: string[];
25
+ poolOptions?: { maxActive?: number; idleTimeoutMs?: number };
26
+ }
27
+
28
+ export async function runLauncher(
29
+ configOrConfigs: AgentConfig | AgentConfig[],
30
+ opts: FleetOptions = {},
31
+ ): Promise<unknown> {
32
+ if (Array.isArray(configOrConfigs)) return runFleet(configOrConfigs, opts);
33
+ return runSingle(configOrConfigs, opts as SingleOptions);
34
+ }
35
+
36
+ async function runSingle(
37
+ config: AgentConfig,
38
+ { operatorKey, label = 'agent', port = process.env.PORT || 8080, healthServer = true, onShutdown }: SingleOptions = {},
39
+ ): Promise<{ agent: unknown; healthServer?: Server }> {
40
+ const effectiveKey = operatorKey ?? process.env.ZIGGS_OPERATOR_KEY;
41
+ const decorated = effectiveKey && !config.operatorKey ? { ...config, operatorKey: effectiveKey } : config;
42
+ const agent = new AgentHost(decorated);
43
+ await agent.connectAsync();
44
+ runtimeLog.info(
45
+ label,
46
+ `"${(agent as unknown as Record<string, unknown>)['options'] ? ((agent as unknown as Record<string, {agentId?: string; name?: string | null}>)['options']?.agentId ?? (agent as unknown as Record<string, {name?: string | null}>)['options']?.name) : 'agent'}" connected`,
47
+ );
48
+
49
+ const server = healthServer ? createHealthServer({ port: Number(port), label }) : null;
50
+
51
+ process.on('SIGTERM', async () => {
52
+ runtimeLog.info(label, 'SIGTERM — shutting down');
53
+ agent.disconnect();
54
+ try {
55
+ await onShutdown?.();
56
+ } catch (err) {
57
+ runtimeLog.warn(label, `onShutdown error: ${(err as Error).message}`);
58
+ }
59
+ server?.close();
60
+ process.exit(0);
61
+ });
62
+
63
+ return { agent, healthServer: server ?? undefined };
64
+ }
65
+
66
+ async function runFleet(
67
+ configs: AgentConfig[],
68
+ {
69
+ operatorKey, wsUrl = process.env.WS_URL, port = process.env.PORT || 8080,
70
+ healthServer = true, label = 'launcher', agentMeta,
71
+ orchestrator: orchestratorConfig, preWake = [], poolOptions, onShutdown,
72
+ }: FleetOptions = {},
73
+ ): Promise<{ pool: ConnectionPool; orchestrator: unknown; healthServer?: Server }> {
74
+ const pool = new ConnectionPool(poolOptions);
75
+ const effectiveKey = operatorKey ?? process.env.ZIGGS_OPERATOR_KEY;
76
+ const decoratedConfigs = effectiveKey
77
+ ? configs.map(c => (c.operatorKey ? c : { ...c, operatorKey: effectiveKey }))
78
+ : configs;
79
+ pool.register(decoratedConfigs, agentMeta as Record<string, unknown>[]);
80
+ const server = healthServer ? createHealthServer({ port: Number(port), label }) : null;
81
+
82
+ let resolvedOrchConfig: AgentConfig | undefined = typeof orchestratorConfig === 'function'
83
+ ? orchestratorConfig(pool)
84
+ : orchestratorConfig;
85
+ if (resolvedOrchConfig && effectiveKey && !resolvedOrchConfig.operatorKey) {
86
+ resolvedOrchConfig = { ...resolvedOrchConfig, operatorKey: effectiveKey };
87
+ }
88
+
89
+ const controlKey = effectiveKey ?? configs.find(c => c.operatorKey)?.operatorKey ?? resolvedOrchConfig?.operatorKey;
90
+ if (controlKey) {
91
+ pool.startControl({ wsUrl, operatorKey: controlKey });
92
+ } else {
93
+ runtimeLog.warn(label, 'No operatorKey provided — skipping control socket');
94
+ }
95
+
96
+ let orchestrator: unknown = null;
97
+ if (resolvedOrchConfig) {
98
+ orchestrator = new AgentHost(resolvedOrchConfig);
99
+ await (orchestrator as { connectAsync: () => Promise<void> }).connectAsync();
100
+ runtimeLog.info(
101
+ label,
102
+ `Orchestrator "${resolvedOrchConfig.name || resolvedOrchConfig.ownAgentId || 'orchestrator'}" connected`,
103
+ );
104
+ }
105
+
106
+ if (preWake.length > 0) {
107
+ await Promise.all(preWake.map(id =>
108
+ pool.wake(id)
109
+ .then(() => runtimeLog.debug(label, `Pre-woke "${id}"`))
110
+ .catch(err => runtimeLog.warn(label, `Could not pre-wake "${id}": ${(err as Error).message}`)),
111
+ ));
112
+ }
113
+
114
+ runtimeLog.info(label, `Ready — ${pool.size} agent(s) registered, ${pool.listActive().length} connected`);
115
+
116
+ process.on('SIGTERM', async () => {
117
+ runtimeLog.info(label, 'SIGTERM — shutting down');
118
+ pool.stopControl();
119
+ (orchestrator as { disconnect?: () => void })?.disconnect?.();
120
+ await pool.disconnectAll();
121
+ try {
122
+ await onShutdown?.();
123
+ } catch (err) {
124
+ runtimeLog.warn(label, `onShutdown error: ${(err as Error).message}`);
125
+ }
126
+ server?.close();
127
+ process.exit(0);
128
+ });
129
+
130
+ return { pool, orchestrator, healthServer: server ?? undefined };
131
+ }