@pikku/kysely 0.12.8 → 0.12.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.10
4
+
5
+ ### Patch Changes
6
+
7
+ - 311c0c4: Unify session persistence through SessionStore, remove session blob from ChannelStore
8
+
9
+ - PikkuSessionService now persists sessions via SessionStore on set()/clear() instead of every function call
10
+ - ChannelStore no longer stores session data — maps channelId to pikkuUserId only
11
+ - ChannelStore API: setUserSession/getChannelAndSession replaced with setPikkuUserId/getChannel
12
+ - Serverless channel runner resolves sessions from SessionStore using pikkuUserId from ChannelStore
13
+
14
+ - Updated dependencies [311c0c4]
15
+ - @pikku/core@0.12.18
16
+
17
+ ## 0.12.9
18
+
19
+ ### Patch Changes
20
+
21
+ - 624097e: Add deploy pipeline with provider-agnostic architecture
22
+
23
+ - Add MetaService with explicit typed API, absorb WiringService reads
24
+ - Add deployment service, traceId propagation, scoped logger
25
+ - Rewrite analyzer: one function = one worker, gateways dispatch via RPC
26
+ - Add Cloudflare deploy provider with plan/apply commands
27
+ - Add per-unit filtered codegen for deploy pipeline
28
+ - Skip missing metadata in wiring registration for deploy units
29
+ - Fix schema coercion crash when schema has no properties
30
+ - Fix E2E codegen: double-pass resolves cross-package Zod type imports
31
+
32
+ - Updated dependencies [9e8605f]
33
+ - Updated dependencies [624097e]
34
+ - Updated dependencies [7ab3243]
35
+ - @pikku/core@0.12.15
36
+
3
37
  ## 0.12.8
4
38
 
5
39
  ### Patch Changes
@@ -4,11 +4,13 @@ export { KyselyWorkflowService } from './kysely-workflow-service.js';
4
4
  export { KyselyWorkflowRunService } from './kysely-workflow-run-service.js';
5
5
  export { KyselyDeploymentService } from './kysely-deployment-service.js';
6
6
  export { KyselyAIStorageService } from './kysely-ai-storage-service.js';
7
- export { KyselyAgentRunService } from './kysely-agent-run-service.js';
7
+ export { KyselyAgentRunService } from './kysely-ai-agent-run-service.js';
8
+ export { KyselyAIRunStateService } from './kysely-ai-run-state-service.js';
8
9
  export { KyselySecretService } from './kysely-secret-service.js';
9
10
  export type { KyselySecretServiceConfig } from './kysely-secret-service.js';
10
11
  export { KyselyCredentialService } from './kysely-credential-service.js';
11
12
  export type { KyselyCredentialServiceConfig } from './kysely-credential-service.js';
13
+ export { KyselySessionStore } from './kysely-session-store.js';
12
14
  export type { KyselyPikkuDB } from './kysely-tables.js';
13
15
  export type { WorkflowRunService } from '@pikku/core/workflow';
14
16
  export type { AgentRunService, AgentRunRow } from '@pikku/core/ai-agent';
package/dist/src/index.js CHANGED
@@ -4,6 +4,8 @@ export { KyselyWorkflowService } from './kysely-workflow-service.js';
4
4
  export { KyselyWorkflowRunService } from './kysely-workflow-run-service.js';
5
5
  export { KyselyDeploymentService } from './kysely-deployment-service.js';
6
6
  export { KyselyAIStorageService } from './kysely-ai-storage-service.js';
7
- export { KyselyAgentRunService } from './kysely-agent-run-service.js';
7
+ export { KyselyAgentRunService } from './kysely-ai-agent-run-service.js';
8
+ export { KyselyAIRunStateService } from './kysely-ai-run-state-service.js';
8
9
  export { KyselySecretService } from './kysely-secret-service.js';
9
10
  export { KyselyCredentialService } from './kysely-credential-service.js';
11
+ export { KyselySessionStore } from './kysely-session-store.js';
@@ -0,0 +1,20 @@
1
+ import type { Kysely } from 'kysely';
2
+ import type { KyselyPikkuDB } from './kysely-tables.js';
3
+ import type { AIRunStateService, CreateRunInput } from '@pikku/core/services';
4
+ import type { AgentRunState, PendingApproval } from '@pikku/core/ai-agent';
5
+ export declare class KyselyAIRunStateService implements AIRunStateService {
6
+ private db;
7
+ private initialized;
8
+ constructor(db: Kysely<KyselyPikkuDB>);
9
+ init(): Promise<void>;
10
+ createRun(run: CreateRunInput): Promise<string>;
11
+ updateRun(runId: string, updates: Partial<AgentRunState>): Promise<void>;
12
+ getRun(runId: string): Promise<AgentRunState | null>;
13
+ getRunsByThread(threadId: string): Promise<AgentRunState[]>;
14
+ resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<void>;
15
+ findRunByToolCallId(toolCallId: string): Promise<{
16
+ run: AgentRunState;
17
+ approval: PendingApproval;
18
+ } | null>;
19
+ private toRunState;
20
+ }
@@ -0,0 +1,172 @@
1
+ import { sql } from 'kysely';
2
+ export class KyselyAIRunStateService {
3
+ db;
4
+ initialized = false;
5
+ constructor(db) {
6
+ this.db = db;
7
+ }
8
+ async init() {
9
+ if (this.initialized)
10
+ return;
11
+ await this.db.schema
12
+ .createTable('aiRun')
13
+ .ifNotExists()
14
+ .addColumn('runId', 'text', (col) => col.primaryKey())
15
+ .addColumn('agentName', 'text', (col) => col.notNull())
16
+ .addColumn('threadId', 'text', (col) => col.notNull())
17
+ .addColumn('resourceId', 'text', (col) => col.notNull())
18
+ .addColumn('status', 'text', (col) => col.defaultTo('running').notNull())
19
+ .addColumn('errorMessage', 'text')
20
+ .addColumn('suspendReason', 'text')
21
+ .addColumn('missingRpcs', 'text')
22
+ .addColumn('pendingApprovals', 'text')
23
+ .addColumn('usageInputTokens', 'integer', (col) => col.defaultTo(0))
24
+ .addColumn('usageOutputTokens', 'integer', (col) => col.defaultTo(0))
25
+ .addColumn('usageModel', 'text', (col) => col.defaultTo(''))
26
+ .addColumn('createdAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
27
+ .addColumn('updatedAt', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
28
+ .execute();
29
+ this.initialized = true;
30
+ }
31
+ async createRun(run) {
32
+ const runId = `run-${crypto.randomUUID()}`;
33
+ await this.db
34
+ .insertInto('aiRun')
35
+ .values({
36
+ runId,
37
+ agentName: run.agentName,
38
+ threadId: run.threadId,
39
+ resourceId: run.resourceId,
40
+ status: run.status ?? 'running',
41
+ errorMessage: run.errorMessage ?? null,
42
+ suspendReason: run.suspendReason ?? null,
43
+ missingRpcs: run.missingRpcs ? JSON.stringify(run.missingRpcs) : null,
44
+ usageInputTokens: run.usage?.inputTokens ?? 0,
45
+ usageOutputTokens: run.usage?.outputTokens ?? 0,
46
+ usageModel: run.usage?.model ?? '',
47
+ })
48
+ .execute();
49
+ return runId;
50
+ }
51
+ async updateRun(runId, updates) {
52
+ const values = {
53
+ updatedAt: sql `CURRENT_TIMESTAMP`,
54
+ };
55
+ if (updates.status !== undefined)
56
+ values.status = updates.status;
57
+ if (updates.errorMessage !== undefined)
58
+ values.errorMessage = updates.errorMessage;
59
+ if (updates.suspendReason !== undefined)
60
+ values.suspendReason = updates.suspendReason;
61
+ if (updates.missingRpcs !== undefined)
62
+ values.missingRpcs = JSON.stringify(updates.missingRpcs);
63
+ if (updates.pendingApprovals !== undefined)
64
+ values.pendingApprovals = JSON.stringify(updates.pendingApprovals);
65
+ if (updates.usage) {
66
+ values.usageInputTokens = updates.usage.inputTokens;
67
+ values.usageOutputTokens = updates.usage.outputTokens;
68
+ values.usageModel = updates.usage.model;
69
+ }
70
+ await this.db
71
+ .updateTable('aiRun')
72
+ .set(values)
73
+ .where('runId', '=', runId)
74
+ .execute();
75
+ }
76
+ async getRun(runId) {
77
+ const row = await this.db
78
+ .selectFrom('aiRun')
79
+ .selectAll()
80
+ .where('runId', '=', runId)
81
+ .executeTakeFirst();
82
+ return row ? this.toRunState(row) : null;
83
+ }
84
+ async getRunsByThread(threadId) {
85
+ const rows = await this.db
86
+ .selectFrom('aiRun')
87
+ .selectAll()
88
+ .where('threadId', '=', threadId)
89
+ .orderBy('createdAt', 'desc')
90
+ .execute();
91
+ return rows.map((r) => this.toRunState(r));
92
+ }
93
+ async resolveApproval(toolCallId, status) {
94
+ const rows = await this.db
95
+ .selectFrom('aiRun')
96
+ .select(['runId', 'pendingApprovals'])
97
+ .where('status', '=', 'suspended')
98
+ .execute();
99
+ for (const row of rows) {
100
+ let approvals = [];
101
+ if (row.pendingApprovals) {
102
+ try {
103
+ approvals = JSON.parse(row.pendingApprovals);
104
+ }
105
+ catch {
106
+ console.warn(`Failed to parse pendingApprovals for run ${row.runId}, treating as empty`);
107
+ }
108
+ }
109
+ const filtered = approvals.filter((a) => a.toolCallId !== toolCallId);
110
+ if (filtered.length !== approvals.length) {
111
+ const updates = {
112
+ pendingApprovals: filtered.length > 0 ? JSON.stringify(filtered) : null,
113
+ updatedAt: sql `CURRENT_TIMESTAMP`,
114
+ };
115
+ if (filtered.length === 0) {
116
+ updates.status = status;
117
+ }
118
+ await this.db
119
+ .updateTable('aiRun')
120
+ .set(updates)
121
+ .where('runId', '=', row.runId)
122
+ .execute();
123
+ return;
124
+ }
125
+ }
126
+ }
127
+ async findRunByToolCallId(toolCallId) {
128
+ const rows = await this.db
129
+ .selectFrom('aiRun')
130
+ .selectAll()
131
+ .where('status', '=', 'suspended')
132
+ .execute();
133
+ for (const row of rows) {
134
+ let approvals = [];
135
+ if (row.pendingApprovals) {
136
+ try {
137
+ approvals = JSON.parse(row.pendingApprovals);
138
+ }
139
+ catch {
140
+ console.warn(`Failed to parse pendingApprovals for run ${row.runId}, treating as empty`);
141
+ }
142
+ }
143
+ const approval = approvals.find((a) => a.toolCallId === toolCallId);
144
+ if (approval) {
145
+ return { run: this.toRunState(row), approval };
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ toRunState(row) {
151
+ return {
152
+ runId: row.runId,
153
+ agentName: row.agentName,
154
+ threadId: row.threadId,
155
+ resourceId: row.resourceId,
156
+ status: row.status,
157
+ errorMessage: row.errorMessage ?? undefined,
158
+ suspendReason: row.suspendReason ?? undefined,
159
+ missingRpcs: row.missingRpcs ? JSON.parse(row.missingRpcs) : undefined,
160
+ pendingApprovals: row.pendingApprovals
161
+ ? JSON.parse(row.pendingApprovals)
162
+ : undefined,
163
+ usage: {
164
+ inputTokens: row.usageInputTokens ?? 0,
165
+ outputTokens: row.usageOutputTokens ?? 0,
166
+ model: row.usageModel ?? '',
167
+ },
168
+ createdAt: new Date(row.createdAt),
169
+ updatedAt: new Date(row.updatedAt),
170
+ };
171
+ }
172
+ }
@@ -1,4 +1,3 @@
1
- import type { CoreUserSession } from '@pikku/core';
2
1
  import type { Channel } from '@pikku/core/channel';
3
2
  import { ChannelStore } from '@pikku/core/channel';
4
3
  import type { Kysely } from 'kysely';
@@ -10,9 +9,9 @@ export declare class KyselyChannelStore extends ChannelStore {
10
9
  init(): Promise<void>;
11
10
  addChannel({ channelId, channelName, openingData, }: Channel): Promise<void>;
12
11
  removeChannels(channelIds: string[]): Promise<void>;
13
- setUserSession(channelId: string, session: CoreUserSession | null): Promise<void>;
14
- getChannelAndSession(channelId: string): Promise<Channel & {
15
- session: CoreUserSession;
12
+ setPikkuUserId(channelId: string, pikkuUserId: string | null): Promise<void>;
13
+ getChannel(channelId: string): Promise<Channel & {
14
+ pikkuUserId?: string;
16
15
  }>;
17
16
  close(): Promise<void>;
18
17
  }
@@ -19,7 +19,7 @@ export class KyselyChannelStore extends ChannelStore {
19
19
  .addColumn('channel_name', 'text', (col) => col.notNull())
20
20
  .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
21
21
  .addColumn('opening_data', 'text', (col) => col.notNull().defaultTo('{}'))
22
- .addColumn('user_session', 'text')
22
+ .addColumn('pikku_user_id', 'text')
23
23
  .addColumn('last_wire', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
24
24
  .execute();
25
25
  await this.db.schema
@@ -53,17 +53,17 @@ export class KyselyChannelStore extends ChannelStore {
53
53
  .where('channelId', 'in', channelIds)
54
54
  .execute();
55
55
  }
56
- async setUserSession(channelId, session) {
56
+ async setPikkuUserId(channelId, pikkuUserId) {
57
57
  await this.db
58
58
  .updateTable('channels')
59
- .set({ userSession: session ? JSON.stringify(session) : null })
59
+ .set({ pikkuUserId })
60
60
  .where('channelId', '=', channelId)
61
61
  .execute();
62
62
  }
63
- async getChannelAndSession(channelId) {
63
+ async getChannel(channelId) {
64
64
  const row = await this.db
65
65
  .selectFrom('channels')
66
- .select(['channelId', 'channelName', 'openingData', 'userSession'])
66
+ .select(['channelId', 'channelName', 'openingData', 'pikkuUserId'])
67
67
  .where('channelId', '=', channelId)
68
68
  .executeTakeFirst();
69
69
  if (!row) {
@@ -73,7 +73,7 @@ export class KyselyChannelStore extends ChannelStore {
73
73
  channelId: row.channelId,
74
74
  channelName: row.channelName,
75
75
  openingData: parseJson(row.openingData) ?? {},
76
- session: (parseJson(row.userSession) ?? {}),
76
+ pikkuUserId: row.pikkuUserId ?? undefined,
77
77
  };
78
78
  }
79
79
  async close() { }
@@ -1,18 +1,21 @@
1
- import type { DeploymentService, DeploymentServiceConfig, DeploymentConfig, DeploymentInfo } from '@pikku/core/services';
1
+ import type { DeploymentService, DeploymentServiceConfig, DeploymentConfig } from '@pikku/core/services';
2
+ import type { JWTService, SecretService } from '@pikku/core/services';
2
3
  import type { Kysely } from 'kysely';
3
4
  import type { KyselyPikkuDB } from './kysely-tables.js';
4
5
  export declare class KyselyDeploymentService implements DeploymentService {
5
6
  protected db: Kysely<KyselyPikkuDB>;
7
+ private jwt?;
8
+ private secrets?;
6
9
  private initialized;
7
10
  private heartbeatTimer?;
8
11
  private deploymentConfig?;
9
12
  private heartbeatInterval;
10
13
  private heartbeatTtl;
11
- constructor(config: DeploymentServiceConfig, db: Kysely<KyselyPikkuDB>);
14
+ constructor(config: DeploymentServiceConfig, db: Kysely<KyselyPikkuDB>, jwt?: JWTService | undefined, secrets?: SecretService | undefined);
12
15
  init(): Promise<void>;
13
16
  start(config: DeploymentConfig): Promise<void>;
14
17
  stop(): Promise<void>;
15
- findFunction(name: string): Promise<DeploymentInfo[]>;
18
+ invoke(funcName: string, data: unknown, session?: unknown, traceId?: string): Promise<unknown>;
16
19
  private createIndexSafe;
17
20
  private sendHeartbeat;
18
21
  }
@@ -1,14 +1,19 @@
1
+ import { buildRemoteHeaders } from '@pikku/core/remote';
1
2
  import { getAllFunctionNames } from '@pikku/core/function';
2
3
  import { sql } from 'kysely';
3
4
  export class KyselyDeploymentService {
4
5
  db;
6
+ jwt;
7
+ secrets;
5
8
  initialized = false;
6
9
  heartbeatTimer;
7
10
  deploymentConfig;
8
11
  heartbeatInterval;
9
12
  heartbeatTtl;
10
- constructor(config, db) {
13
+ constructor(config, db, jwt, secrets) {
11
14
  this.db = db;
15
+ this.jwt = jwt;
16
+ this.secrets = secrets;
12
17
  this.heartbeatInterval = config.heartbeatInterval ?? 10000;
13
18
  this.heartbeatTtl = config.heartbeatTtl ?? 30000;
14
19
  }
@@ -93,21 +98,36 @@ export class KyselyDeploymentService {
93
98
  .execute();
94
99
  }
95
100
  }
96
- async findFunction(name) {
101
+ async invoke(funcName, data, session, traceId) {
102
+ const headers = await buildRemoteHeaders(this.jwt, this.secrets, funcName, session, traceId);
97
103
  const ttlMs = this.heartbeatTtl;
98
104
  const cutoff = new Date(Date.now() - ttlMs);
99
105
  const result = await this.db
100
106
  .selectFrom('pikkuDeployments as d')
101
107
  .innerJoin('pikkuDeploymentFunctions as f', 'f.deploymentId', 'd.deploymentId')
102
108
  .select(['d.deploymentId', 'd.endpoint'])
103
- .where('f.functionName', '=', name)
109
+ .where('f.functionName', '=', funcName)
104
110
  .where('d.lastHeartbeat', '>', cutoff)
105
111
  .orderBy('d.lastHeartbeat', 'desc')
112
+ .limit(1)
106
113
  .execute();
107
- return result.map((row) => ({
108
- deploymentId: row.deploymentId,
109
- endpoint: row.endpoint,
110
- }));
114
+ if (result.length === 0) {
115
+ throw new Error(`No deployment found for function '${funcName}'`);
116
+ }
117
+ const endpoint = result[0].endpoint;
118
+ const url = `${endpoint}/remote/rpc/${encodeURIComponent(funcName)}`;
119
+ const response = await fetch(url, {
120
+ method: 'POST',
121
+ headers: {
122
+ 'content-type': 'application/json',
123
+ ...headers,
124
+ },
125
+ body: JSON.stringify({ data }),
126
+ });
127
+ if (!response.ok) {
128
+ throw new Error(`Remote RPC call to '${funcName}' failed: ${response.status}`);
129
+ }
130
+ return response.json();
111
131
  }
112
132
  async createIndexSafe(builder) {
113
133
  try {
@@ -0,0 +1,13 @@
1
+ import type { CoreUserSession } from '@pikku/core';
2
+ import type { SessionStore } from '@pikku/core/services';
3
+ import type { Kysely } from 'kysely';
4
+ import type { KyselyPikkuDB } from './kysely-tables.js';
5
+ export declare class KyselySessionStore implements SessionStore {
6
+ private db;
7
+ private initialized;
8
+ constructor(db: Kysely<KyselyPikkuDB>);
9
+ init(): Promise<void>;
10
+ get(pikkuUserId: string): Promise<CoreUserSession | undefined>;
11
+ set(pikkuUserId: string, session: CoreUserSession): Promise<void>;
12
+ clear(pikkuUserId: string): Promise<void>;
13
+ }
@@ -0,0 +1,54 @@
1
+ import { sql } from 'kysely';
2
+ import { parseJson } from './kysely-json.js';
3
+ export class KyselySessionStore {
4
+ db;
5
+ initialized = false;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ async init() {
10
+ if (this.initialized) {
11
+ return;
12
+ }
13
+ await this.db.schema
14
+ .createTable('pikku_user_sessions')
15
+ .ifNotExists()
16
+ .addColumn('pikku_user_id', 'text', (col) => col.primaryKey())
17
+ .addColumn('session', 'text', (col) => col.notNull())
18
+ .addColumn('created_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
19
+ .addColumn('updated_at', 'timestamp', (col) => col.defaultTo(sql `CURRENT_TIMESTAMP`).notNull())
20
+ .execute();
21
+ this.initialized = true;
22
+ }
23
+ async get(pikkuUserId) {
24
+ const row = await this.db
25
+ .selectFrom('pikkuUserSessions')
26
+ .select(['session'])
27
+ .where('pikkuUserId', '=', pikkuUserId)
28
+ .executeTakeFirst();
29
+ if (!row) {
30
+ return undefined;
31
+ }
32
+ return (parseJson(row.session) ?? undefined);
33
+ }
34
+ async set(pikkuUserId, session) {
35
+ await this.db
36
+ .insertInto('pikkuUserSessions')
37
+ .values({
38
+ pikkuUserId,
39
+ session: JSON.stringify(session),
40
+ updatedAt: new Date(),
41
+ })
42
+ .onConflict((oc) => oc.column('pikkuUserId').doUpdateSet({
43
+ session: JSON.stringify(session),
44
+ updatedAt: new Date(),
45
+ }))
46
+ .execute();
47
+ }
48
+ async clear(pikkuUserId) {
49
+ await this.db
50
+ .deleteFrom('pikkuUserSessions')
51
+ .where('pikkuUserId', '=', pikkuUserId)
52
+ .execute();
53
+ }
54
+ }
@@ -5,7 +5,7 @@ export interface ChannelsTable {
5
5
  channelName: string;
6
6
  createdAt: Generated<Date>;
7
7
  openingData: string;
8
- userSession: string | null;
8
+ pikkuUserId: string | null;
9
9
  lastWire: Generated<Date>;
10
10
  }
11
11
  export interface ChannelSubscriptionsTable {
@@ -153,6 +153,12 @@ export interface CredentialsAuditTable {
153
153
  action: string;
154
154
  performedAt: Generated<Date>;
155
155
  }
156
+ export interface UserSessionsTable {
157
+ pikkuUserId: string;
158
+ session: string;
159
+ createdAt: Generated<Date>;
160
+ updatedAt: Generated<Date>;
161
+ }
156
162
  export interface KyselyPikkuDB {
157
163
  channels: ChannelsTable;
158
164
  channelSubscriptions: ChannelSubscriptionsTable;
@@ -171,4 +177,5 @@ export interface KyselyPikkuDB {
171
177
  secretsAudit: SecretsAuditTable;
172
178
  credentials: CredentialsTable;
173
179
  credentialsAudit: CredentialsAuditTable;
180
+ pikkuUserSessions: UserSessionsTable;
174
181
  }
@@ -175,7 +175,7 @@ export class KyselyWorkflowRunService {
175
175
  let query = this.db
176
176
  .selectFrom('workflowVersions')
177
177
  .select(['workflowName', 'graphHash', 'graph'])
178
- .where('source', '=', 'ai-agent')
178
+ .where('source', '=', 'dynamic-workflow')
179
179
  .where('status', '=', 'active');
180
180
  if (agentName) {
181
181
  query = query.where('workflowName', 'like', `ai:${agentName}:%`);
@@ -105,8 +105,8 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
105
105
  status: 'running',
106
106
  input: JSON.stringify(input),
107
107
  inline,
108
- graphHash: graphHash,
109
- wire: JSON.stringify(wire),
108
+ graphHash: graphHash ?? null,
109
+ wire: wire ? JSON.stringify(wire) : null,
110
110
  })
111
111
  .execute();
112
112
  return id;
@@ -486,7 +486,20 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
486
486
  return this.runService.getWorkflowVersion(name, graphHash);
487
487
  }
488
488
  async getAIGeneratedWorkflows(agentName) {
489
- return this.runService.getAIGeneratedWorkflows(agentName);
489
+ let query = this.db
490
+ .selectFrom('workflowVersions')
491
+ .select(['workflowName', 'graphHash', 'graph'])
492
+ .where('source', '=', 'dynamic-workflow')
493
+ .where('status', '=', 'active');
494
+ if (agentName) {
495
+ query = query.where('workflowName', 'like', `ai:${agentName}:%`);
496
+ }
497
+ const rows = await query.execute();
498
+ return rows.map((row) => ({
499
+ workflowName: row.workflowName,
500
+ graphHash: row.graphHash,
501
+ graph: typeof row.graph === 'string' ? JSON.parse(row.graph) : row.graph,
502
+ }));
490
503
  }
491
504
  async close() { }
492
505
  }
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/kysely-agent-run-service.ts","../src/kysely-ai-storage-service.ts","../src/kysely-channel-store.ts","../src/kysely-credential-service.ts","../src/kysely-deployment-service.ts","../src/kysely-eventhub-store.ts","../src/kysely-json.ts","../src/kysely-secret-service.ts","../src/kysely-tables.ts","../src/kysely-workflow-run-service.ts","../src/kysely-workflow-service.ts","../bin/pikku-kysely-pure.ts"],"version":"5.9.3"}
1
+ {"root":["../src/index.ts","../src/kysely-ai-agent-run-service.ts","../src/kysely-ai-run-state-service.ts","../src/kysely-ai-storage-service.ts","../src/kysely-channel-store.ts","../src/kysely-credential-service.ts","../src/kysely-deployment-service.ts","../src/kysely-eventhub-store.ts","../src/kysely-json.ts","../src/kysely-secret-service.ts","../src/kysely-session-store.ts","../src/kysely-tables.ts","../src/kysely-workflow-run-service.ts","../src/kysely-workflow-service.ts","../bin/pikku-kysely-pure.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/kysely",
3
- "version": "0.12.8",
3
+ "version": "0.12.10",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "module": "dist/src/index.js",
@@ -20,7 +20,7 @@
20
20
  "prepublishOnly": "yarn build"
21
21
  },
22
22
  "peerDependencies": {
23
- "@pikku/core": "^0.12.14"
23
+ "@pikku/core": "^0.12.18"
24
24
  },
25
25
  "dependencies": {
26
26
  "kysely": "^0.28.12"
package/src/index.ts CHANGED
@@ -4,11 +4,13 @@ export { KyselyWorkflowService } from './kysely-workflow-service.js'
4
4
  export { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
5
5
  export { KyselyDeploymentService } from './kysely-deployment-service.js'
6
6
  export { KyselyAIStorageService } from './kysely-ai-storage-service.js'
7
- export { KyselyAgentRunService } from './kysely-agent-run-service.js'
7
+ export { KyselyAgentRunService } from './kysely-ai-agent-run-service.js'
8
+ export { KyselyAIRunStateService } from './kysely-ai-run-state-service.js'
8
9
  export { KyselySecretService } from './kysely-secret-service.js'
9
10
  export type { KyselySecretServiceConfig } from './kysely-secret-service.js'
10
11
  export { KyselyCredentialService } from './kysely-credential-service.js'
11
12
  export type { KyselyCredentialServiceConfig } from './kysely-credential-service.js'
13
+ export { KyselySessionStore } from './kysely-session-store.js'
12
14
 
13
15
  export type { KyselyPikkuDB } from './kysely-tables.js'
14
16
  export type { WorkflowRunService } from '@pikku/core/workflow'
@@ -0,0 +1,197 @@
1
+ import { sql } from 'kysely'
2
+ import type { Kysely } from 'kysely'
3
+ import type { KyselyPikkuDB } from './kysely-tables.js'
4
+ import type { AIRunStateService, CreateRunInput } from '@pikku/core/services'
5
+ import type { AgentRunState, PendingApproval } from '@pikku/core/ai-agent'
6
+
7
+ export class KyselyAIRunStateService implements AIRunStateService {
8
+ private initialized = false
9
+
10
+ constructor(private db: Kysely<KyselyPikkuDB>) {}
11
+
12
+ async init(): Promise<void> {
13
+ if (this.initialized) return
14
+
15
+ await this.db.schema
16
+ .createTable('aiRun')
17
+ .ifNotExists()
18
+ .addColumn('runId', 'text', (col) => col.primaryKey())
19
+ .addColumn('agentName', 'text', (col) => col.notNull())
20
+ .addColumn('threadId', 'text', (col) => col.notNull())
21
+ .addColumn('resourceId', 'text', (col) => col.notNull())
22
+ .addColumn('status', 'text', (col) => col.defaultTo('running').notNull())
23
+ .addColumn('errorMessage', 'text')
24
+ .addColumn('suspendReason', 'text')
25
+ .addColumn('missingRpcs', 'text')
26
+ .addColumn('pendingApprovals', 'text')
27
+ .addColumn('usageInputTokens', 'integer', (col) => col.defaultTo(0))
28
+ .addColumn('usageOutputTokens', 'integer', (col) => col.defaultTo(0))
29
+ .addColumn('usageModel', 'text', (col) => col.defaultTo(''))
30
+ .addColumn('createdAt', 'timestamp', (col) =>
31
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
32
+ )
33
+ .addColumn('updatedAt', 'timestamp', (col) =>
34
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
35
+ )
36
+ .execute()
37
+
38
+ this.initialized = true
39
+ }
40
+
41
+ async createRun(run: CreateRunInput): Promise<string> {
42
+ const runId = `run-${crypto.randomUUID()}`
43
+ await this.db
44
+ .insertInto('aiRun')
45
+ .values({
46
+ runId,
47
+ agentName: run.agentName,
48
+ threadId: run.threadId,
49
+ resourceId: run.resourceId,
50
+ status: run.status ?? 'running',
51
+ errorMessage: run.errorMessage ?? null,
52
+ suspendReason: run.suspendReason ?? null,
53
+ missingRpcs: run.missingRpcs ? JSON.stringify(run.missingRpcs) : null,
54
+ usageInputTokens: run.usage?.inputTokens ?? 0,
55
+ usageOutputTokens: run.usage?.outputTokens ?? 0,
56
+ usageModel: run.usage?.model ?? '',
57
+ })
58
+ .execute()
59
+ return runId
60
+ }
61
+
62
+ async updateRun(
63
+ runId: string,
64
+ updates: Partial<AgentRunState>
65
+ ): Promise<void> {
66
+ const values: Record<string, unknown> = {
67
+ updatedAt: sql`CURRENT_TIMESTAMP`,
68
+ }
69
+ if (updates.status !== undefined) values.status = updates.status
70
+ if (updates.errorMessage !== undefined)
71
+ values.errorMessage = updates.errorMessage
72
+ if (updates.suspendReason !== undefined)
73
+ values.suspendReason = updates.suspendReason
74
+ if (updates.missingRpcs !== undefined)
75
+ values.missingRpcs = JSON.stringify(updates.missingRpcs)
76
+ if (updates.pendingApprovals !== undefined)
77
+ values.pendingApprovals = JSON.stringify(updates.pendingApprovals)
78
+ if (updates.usage) {
79
+ values.usageInputTokens = updates.usage.inputTokens
80
+ values.usageOutputTokens = updates.usage.outputTokens
81
+ values.usageModel = updates.usage.model
82
+ }
83
+
84
+ await this.db
85
+ .updateTable('aiRun')
86
+ .set(values)
87
+ .where('runId', '=', runId)
88
+ .execute()
89
+ }
90
+
91
+ async getRun(runId: string): Promise<AgentRunState | null> {
92
+ const row = await this.db
93
+ .selectFrom('aiRun')
94
+ .selectAll()
95
+ .where('runId', '=', runId)
96
+ .executeTakeFirst()
97
+ return row ? this.toRunState(row) : null
98
+ }
99
+
100
+ async getRunsByThread(threadId: string): Promise<AgentRunState[]> {
101
+ const rows = await this.db
102
+ .selectFrom('aiRun')
103
+ .selectAll()
104
+ .where('threadId', '=', threadId)
105
+ .orderBy('createdAt', 'desc')
106
+ .execute()
107
+ return rows.map((r) => this.toRunState(r))
108
+ }
109
+
110
+ async resolveApproval(
111
+ toolCallId: string,
112
+ status: 'approved' | 'denied'
113
+ ): Promise<void> {
114
+ const rows = await this.db
115
+ .selectFrom('aiRun')
116
+ .select(['runId', 'pendingApprovals' as any])
117
+ .where('status', '=', 'suspended')
118
+ .execute()
119
+
120
+ for (const row of rows) {
121
+ let approvals: PendingApproval[] = []
122
+ if (row.pendingApprovals) {
123
+ try {
124
+ approvals = JSON.parse(row.pendingApprovals as string)
125
+ } catch {
126
+ console.warn(`Failed to parse pendingApprovals for run ${row.runId}, treating as empty`)
127
+ }
128
+ }
129
+ const filtered = approvals.filter((a) => a.toolCallId !== toolCallId)
130
+ if (filtered.length !== approvals.length) {
131
+ const updates: Record<string, unknown> = {
132
+ pendingApprovals:
133
+ filtered.length > 0 ? JSON.stringify(filtered) : null,
134
+ updatedAt: sql`CURRENT_TIMESTAMP`,
135
+ }
136
+ if (filtered.length === 0) {
137
+ updates.status = status
138
+ }
139
+ await this.db
140
+ .updateTable('aiRun')
141
+ .set(updates as any)
142
+ .where('runId', '=', row.runId)
143
+ .execute()
144
+ return
145
+ }
146
+ }
147
+ }
148
+
149
+ async findRunByToolCallId(
150
+ toolCallId: string
151
+ ): Promise<{ run: AgentRunState; approval: PendingApproval } | null> {
152
+ const rows = await this.db
153
+ .selectFrom('aiRun')
154
+ .selectAll()
155
+ .where('status', '=', 'suspended')
156
+ .execute()
157
+
158
+ for (const row of rows) {
159
+ let approvals: PendingApproval[] = []
160
+ if ((row as any).pendingApprovals) {
161
+ try {
162
+ approvals = JSON.parse((row as any).pendingApprovals)
163
+ } catch {
164
+ console.warn(`Failed to parse pendingApprovals for run ${row.runId}, treating as empty`)
165
+ }
166
+ }
167
+ const approval = approvals.find((a) => a.toolCallId === toolCallId)
168
+ if (approval) {
169
+ return { run: this.toRunState(row), approval }
170
+ }
171
+ }
172
+ return null
173
+ }
174
+
175
+ private toRunState(row: any): AgentRunState {
176
+ return {
177
+ runId: row.runId,
178
+ agentName: row.agentName,
179
+ threadId: row.threadId,
180
+ resourceId: row.resourceId,
181
+ status: row.status,
182
+ errorMessage: row.errorMessage ?? undefined,
183
+ suspendReason: row.suspendReason ?? undefined,
184
+ missingRpcs: row.missingRpcs ? JSON.parse(row.missingRpcs) : undefined,
185
+ pendingApprovals: row.pendingApprovals
186
+ ? JSON.parse(row.pendingApprovals)
187
+ : undefined,
188
+ usage: {
189
+ inputTokens: row.usageInputTokens ?? 0,
190
+ outputTokens: row.usageOutputTokens ?? 0,
191
+ model: row.usageModel ?? '',
192
+ },
193
+ createdAt: new Date(row.createdAt),
194
+ updatedAt: new Date(row.updatedAt),
195
+ }
196
+ }
197
+ }
@@ -1,4 +1,3 @@
1
- import type { CoreUserSession } from '@pikku/core'
2
1
  import type { Channel } from '@pikku/core/channel'
3
2
  import { ChannelStore } from '@pikku/core/channel'
4
3
  import type { Kysely } from 'kysely'
@@ -27,7 +26,7 @@ export class KyselyChannelStore extends ChannelStore {
27
26
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
28
27
  )
29
28
  .addColumn('opening_data', 'text', (col) => col.notNull().defaultTo('{}'))
30
- .addColumn('user_session', 'text')
29
+ .addColumn('pikku_user_id', 'text')
31
30
  .addColumn('last_wire', 'timestamp', (col) =>
32
31
  col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
33
32
  )
@@ -75,23 +74,23 @@ export class KyselyChannelStore extends ChannelStore {
75
74
  .execute()
76
75
  }
77
76
 
78
- public async setUserSession(
77
+ public async setPikkuUserId(
79
78
  channelId: string,
80
- session: CoreUserSession | null
79
+ pikkuUserId: string | null
81
80
  ): Promise<void> {
82
81
  await this.db
83
82
  .updateTable('channels')
84
- .set({ userSession: session ? JSON.stringify(session) : null })
83
+ .set({ pikkuUserId })
85
84
  .where('channelId', '=', channelId)
86
85
  .execute()
87
86
  }
88
87
 
89
- public async getChannelAndSession(
88
+ public async getChannel(
90
89
  channelId: string
91
- ): Promise<Channel & { session: CoreUserSession }> {
90
+ ): Promise<Channel & { pikkuUserId?: string }> {
92
91
  const row = await this.db
93
92
  .selectFrom('channels')
94
- .select(['channelId', 'channelName', 'openingData', 'userSession'])
93
+ .select(['channelId', 'channelName', 'openingData', 'pikkuUserId'])
95
94
  .where('channelId', '=', channelId)
96
95
  .executeTakeFirst()
97
96
 
@@ -103,7 +102,7 @@ export class KyselyChannelStore extends ChannelStore {
103
102
  channelId: row.channelId,
104
103
  channelName: row.channelName,
105
104
  openingData: parseJson(row.openingData) ?? {},
106
- session: (parseJson(row.userSession) ?? {}) as CoreUserSession,
105
+ pikkuUserId: row.pikkuUserId ?? undefined,
107
106
  }
108
107
  }
109
108
 
@@ -1,9 +1,10 @@
1
+ import { buildRemoteHeaders } from '@pikku/core/remote'
1
2
  import type {
2
3
  DeploymentService,
3
4
  DeploymentServiceConfig,
4
5
  DeploymentConfig,
5
- DeploymentInfo,
6
6
  } from '@pikku/core/services'
7
+ import type { JWTService, SecretService } from '@pikku/core/services'
7
8
  import { getAllFunctionNames } from '@pikku/core/function'
8
9
  import type { Kysely } from 'kysely'
9
10
  import { sql } from 'kysely'
@@ -18,7 +19,9 @@ export class KyselyDeploymentService implements DeploymentService {
18
19
 
19
20
  constructor(
20
21
  config: DeploymentServiceConfig,
21
- protected db: Kysely<KyselyPikkuDB>
22
+ protected db: Kysely<KyselyPikkuDB>,
23
+ private jwt?: JWTService,
24
+ private secrets?: SecretService
22
25
  ) {
23
26
  this.heartbeatInterval = config.heartbeatInterval ?? 10000
24
27
  this.heartbeatTtl = config.heartbeatTtl ?? 30000
@@ -135,7 +138,19 @@ export class KyselyDeploymentService implements DeploymentService {
135
138
  }
136
139
  }
137
140
 
138
- async findFunction(name: string): Promise<DeploymentInfo[]> {
141
+ async invoke(
142
+ funcName: string,
143
+ data: unknown,
144
+ session?: unknown,
145
+ traceId?: string
146
+ ): Promise<unknown> {
147
+ const headers = await buildRemoteHeaders(
148
+ this.jwt,
149
+ this.secrets,
150
+ funcName,
151
+ session,
152
+ traceId
153
+ )
139
154
  const ttlMs = this.heartbeatTtl
140
155
  const cutoff = new Date(Date.now() - ttlMs)
141
156
 
@@ -147,15 +162,34 @@ export class KyselyDeploymentService implements DeploymentService {
147
162
  'd.deploymentId'
148
163
  )
149
164
  .select(['d.deploymentId', 'd.endpoint'])
150
- .where('f.functionName', '=', name)
165
+ .where('f.functionName', '=', funcName)
151
166
  .where('d.lastHeartbeat', '>', cutoff)
152
167
  .orderBy('d.lastHeartbeat', 'desc')
168
+ .limit(1)
153
169
  .execute()
154
170
 
155
- return result.map((row) => ({
156
- deploymentId: row.deploymentId,
157
- endpoint: row.endpoint,
158
- }))
171
+ if (result.length === 0) {
172
+ throw new Error(`No deployment found for function '${funcName}'`)
173
+ }
174
+
175
+ const endpoint = result[0].endpoint
176
+ const url = `${endpoint}/remote/rpc/${encodeURIComponent(funcName)}`
177
+ const response = await fetch(url, {
178
+ method: 'POST',
179
+ headers: {
180
+ 'content-type': 'application/json',
181
+ ...headers,
182
+ },
183
+ body: JSON.stringify({ data }),
184
+ })
185
+
186
+ if (!response.ok) {
187
+ throw new Error(
188
+ `Remote RPC call to '${funcName}' failed: ${response.status}`
189
+ )
190
+ }
191
+
192
+ return response.json()
159
193
  }
160
194
 
161
195
  private async createIndexSafe(builder: {
@@ -14,9 +14,10 @@ import { KyselyWorkflowService } from './kysely-workflow-service.js'
14
14
  import { KyselyWorkflowRunService } from './kysely-workflow-run-service.js'
15
15
  import { KyselyDeploymentService } from './kysely-deployment-service.js'
16
16
  import { KyselyAIStorageService } from './kysely-ai-storage-service.js'
17
- import { KyselyAgentRunService } from './kysely-agent-run-service.js'
17
+ import { KyselyAgentRunService } from './kysely-ai-agent-run-service.js'
18
18
  import { KyselySecretService } from './kysely-secret-service.js'
19
19
  import { KyselyCredentialService } from './kysely-credential-service.js'
20
+ import { KyselySessionStore } from './kysely-session-store.js'
20
21
 
21
22
  function createSqliteDb(): Kysely<KyselyPikkuDB> {
22
23
  return new Kysely<KyselyPikkuDB>({
@@ -109,6 +110,11 @@ function registerTests(
109
110
  await s.init()
110
111
  return s
111
112
  },
113
+ sessionStore: async () => {
114
+ const s = new KyselySessionStore(getDb())
115
+ await s.init()
116
+ return s
117
+ },
112
118
  },
113
119
  })
114
120
 
@@ -0,0 +1,73 @@
1
+ import type { CoreUserSession } from '@pikku/core'
2
+ import type { SessionStore } from '@pikku/core/services'
3
+ import type { Kysely } from 'kysely'
4
+ import { sql } from 'kysely'
5
+ import type { KyselyPikkuDB } from './kysely-tables.js'
6
+ import { parseJson } from './kysely-json.js'
7
+
8
+ export class KyselySessionStore implements SessionStore {
9
+ private initialized = false
10
+
11
+ constructor(private db: Kysely<KyselyPikkuDB>) {}
12
+
13
+ public async init(): Promise<void> {
14
+ if (this.initialized) {
15
+ return
16
+ }
17
+
18
+ await this.db.schema
19
+ .createTable('pikku_user_sessions')
20
+ .ifNotExists()
21
+ .addColumn('pikku_user_id', 'text', (col) => col.primaryKey())
22
+ .addColumn('session', 'text', (col) => col.notNull())
23
+ .addColumn('created_at', 'timestamp', (col) =>
24
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
25
+ )
26
+ .addColumn('updated_at', 'timestamp', (col) =>
27
+ col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
28
+ )
29
+ .execute()
30
+
31
+ this.initialized = true
32
+ }
33
+
34
+ async get(pikkuUserId: string): Promise<CoreUserSession | undefined> {
35
+ const row = await this.db
36
+ .selectFrom('pikkuUserSessions')
37
+ .select(['session'])
38
+ .where('pikkuUserId', '=', pikkuUserId)
39
+ .executeTakeFirst()
40
+
41
+ if (!row) {
42
+ return undefined
43
+ }
44
+
45
+ return (parseJson(row.session) ?? undefined) as
46
+ | CoreUserSession
47
+ | undefined
48
+ }
49
+
50
+ async set(pikkuUserId: string, session: CoreUserSession): Promise<void> {
51
+ await this.db
52
+ .insertInto('pikkuUserSessions')
53
+ .values({
54
+ pikkuUserId,
55
+ session: JSON.stringify(session),
56
+ updatedAt: new Date(),
57
+ })
58
+ .onConflict((oc) =>
59
+ oc.column('pikkuUserId').doUpdateSet({
60
+ session: JSON.stringify(session),
61
+ updatedAt: new Date(),
62
+ })
63
+ )
64
+ .execute()
65
+ }
66
+
67
+ async clear(pikkuUserId: string): Promise<void> {
68
+ await this.db
69
+ .deleteFrom('pikkuUserSessions')
70
+ .where('pikkuUserId', '=', pikkuUserId)
71
+ .execute()
72
+ }
73
+ }
@@ -10,7 +10,7 @@ export interface ChannelsTable {
10
10
  channelName: string
11
11
  createdAt: Generated<Date>
12
12
  openingData: string
13
- userSession: string | null
13
+ pikkuUserId: string | null
14
14
  lastWire: Generated<Date>
15
15
  }
16
16
 
@@ -175,6 +175,13 @@ export interface CredentialsAuditTable {
175
175
  performedAt: Generated<Date>
176
176
  }
177
177
 
178
+ export interface UserSessionsTable {
179
+ pikkuUserId: string
180
+ session: string
181
+ createdAt: Generated<Date>
182
+ updatedAt: Generated<Date>
183
+ }
184
+
178
185
  export interface KyselyPikkuDB {
179
186
  channels: ChannelsTable
180
187
  channelSubscriptions: ChannelSubscriptionsTable
@@ -193,4 +200,5 @@ export interface KyselyPikkuDB {
193
200
  secretsAudit: SecretsAuditTable
194
201
  credentials: CredentialsTable
195
202
  credentialsAudit: CredentialsAuditTable
203
+ pikkuUserSessions: UserSessionsTable
196
204
  }
@@ -223,7 +223,7 @@ export class KyselyWorkflowRunService implements WorkflowRunService {
223
223
  let query = this.db
224
224
  .selectFrom('workflowVersions')
225
225
  .select(['workflowName', 'graphHash', 'graph'])
226
- .where('source', '=', 'ai-agent')
226
+ .where('source', '=', 'dynamic-workflow')
227
227
  .where('status', '=', 'active')
228
228
  if (agentName) {
229
229
  query = query.where('workflowName', 'like', `ai:${agentName}:%`)
@@ -150,8 +150,8 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
150
150
  status: 'running',
151
151
  input: JSON.stringify(input),
152
152
  inline,
153
- graphHash: graphHash,
154
- wire: JSON.stringify(wire),
153
+ graphHash: graphHash ?? null,
154
+ wire: wire ? JSON.stringify(wire) : null,
155
155
  })
156
156
  .execute()
157
157
 
@@ -625,9 +625,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
625
625
  source,
626
626
  status: status ?? 'active',
627
627
  })
628
- .onConflict((oc) =>
629
- oc.columns(['workflowName', 'graphHash']).doNothing()
630
- )
628
+ .onConflict((oc) => oc.columns(['workflowName', 'graphHash']).doNothing())
631
629
  .execute()
632
630
  }
633
631
 
@@ -654,7 +652,20 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
654
652
  async getAIGeneratedWorkflows(
655
653
  agentName?: string
656
654
  ): Promise<Array<{ workflowName: string; graphHash: string; graph: any }>> {
657
- return this.runService.getAIGeneratedWorkflows(agentName)
655
+ let query = this.db
656
+ .selectFrom('workflowVersions')
657
+ .select(['workflowName', 'graphHash', 'graph'])
658
+ .where('source', '=', 'dynamic-workflow')
659
+ .where('status', '=', 'active')
660
+ if (agentName) {
661
+ query = query.where('workflowName', 'like', `ai:${agentName}:%`)
662
+ }
663
+ const rows = await query.execute()
664
+ return rows.map((row) => ({
665
+ workflowName: row.workflowName,
666
+ graphHash: row.graphHash,
667
+ graph: typeof row.graph === 'string' ? JSON.parse(row.graph) : row.graph,
668
+ }))
658
669
  }
659
670
 
660
671
  async close(): Promise<void> {}