@pikku/kysely 0.12.7 → 0.12.9

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,43 @@
1
1
  ## 0.12.0
2
2
 
3
+ ## 0.12.9
4
+
5
+ ### Patch Changes
6
+
7
+ - 624097e: Add deploy pipeline with provider-agnostic architecture
8
+
9
+ - Add MetaService with explicit typed API, absorb WiringService reads
10
+ - Add deployment service, traceId propagation, scoped logger
11
+ - Rewrite analyzer: one function = one worker, gateways dispatch via RPC
12
+ - Add Cloudflare deploy provider with plan/apply commands
13
+ - Add per-unit filtered codegen for deploy pipeline
14
+ - Skip missing metadata in wiring registration for deploy units
15
+ - Fix schema coercion crash when schema has no properties
16
+ - Fix E2E codegen: double-pass resolves cross-package Zod type imports
17
+
18
+ - Updated dependencies [9e8605f]
19
+ - Updated dependencies [624097e]
20
+ - Updated dependencies [7ab3243]
21
+ - @pikku/core@0.12.15
22
+
23
+ ## 0.12.8
24
+
25
+ ### Patch Changes
26
+
27
+ - f85c234: Add unified credential system with per-user OAuth and AI agent pre-flight checks
28
+
29
+ - Unified CredentialService with lazy loading per user via pikkuUserId
30
+ - wire.getCredential() for typed single credential lookup
31
+ - MissingCredentialError with structured payload for client-side connect flows
32
+ - Console UI: Global/Users credential tabs, per-user OAuth connect/revoke
33
+ - AI agent pre-flight check: detects missing OAuth credentials from addon metadata, shows "Connect your accounts" prompt before chat
34
+ - CLI codegen: generates credentialsMeta per addon package for runtime lookup
35
+ - Vercel AI runner: catches MissingCredentialError as runtime fallback
36
+
37
+ - Updated dependencies [f85c234]
38
+ - Updated dependencies [88d3100]
39
+ - @pikku/core@0.12.14
40
+
3
41
  ## 0.12.7
4
42
 
5
43
  ### Patch Changes
@@ -4,7 +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 type { KyselySecretServiceConfig } from './kysely-secret-service.js';
10
11
  export { KyselyCredentialService } from './kysely-credential-service.js';
package/dist/src/index.js CHANGED
@@ -4,6 +4,7 @@ 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';
@@ -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
+ }
@@ -26,5 +26,7 @@ export declare class KyselyCredentialService implements CredentialService {
26
26
  delete(name: string, userId?: string): Promise<void>;
27
27
  has(name: string, userId?: string): Promise<boolean>;
28
28
  getAll(userId: string): Promise<Record<string, unknown>>;
29
+ getUsersWithCredential(name: string): Promise<string[]>;
30
+ getAllUsers(): Promise<string[]>;
29
31
  rotateKEK(): Promise<number>;
30
32
  }
@@ -160,6 +160,24 @@ export class KyselyCredentialService {
160
160
  }
161
161
  return result;
162
162
  }
163
+ async getUsersWithCredential(name) {
164
+ const rows = await this.db
165
+ .selectFrom('credentials')
166
+ .select('userId')
167
+ .where('name', '=', name)
168
+ .where('userId', 'is not', null)
169
+ .execute();
170
+ return rows.map((row) => row.userId).filter(Boolean);
171
+ }
172
+ async getAllUsers() {
173
+ const rows = await this.db
174
+ .selectFrom('credentials')
175
+ .select('userId')
176
+ .distinct()
177
+ .where('userId', 'is not', null)
178
+ .execute();
179
+ return rows.map((row) => row.userId).filter(Boolean);
180
+ }
163
181
  async rotateKEK() {
164
182
  if (!this.previousKey) {
165
183
  throw new Error('No previousKey configured — nothing to rotate from');
@@ -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 {
@@ -105,7 +105,7 @@ export interface AIRunTable {
105
105
  resourceId: string;
106
106
  status: Generated<'running' | 'suspended' | 'completed' | 'failed'>;
107
107
  errorMessage: string | null;
108
- suspendReason: 'approval' | 'rpc-missing' | null;
108
+ suspendReason: 'approval' | 'credential' | 'rpc-missing' | null;
109
109
  missingRpcs: string | null;
110
110
  usageInputTokens: Generated<number>;
111
111
  usageOutputTokens: Generated<number>;
@@ -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-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.7",
3
+ "version": "0.12.9",
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.10"
23
+ "@pikku/core": "^0.12.15"
24
24
  },
25
25
  "dependencies": {
26
26
  "kysely": "^0.28.12"
package/src/index.ts CHANGED
@@ -4,7 +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 type { KyselySecretServiceConfig } from './kysely-secret-service.js'
10
11
  export { KyselyCredentialService } from './kysely-credential-service.js'
@@ -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
+ }
@@ -216,6 +216,28 @@ export class KyselyCredentialService implements CredentialService {
216
216
  return result
217
217
  }
218
218
 
219
+ async getUsersWithCredential(name: string): Promise<string[]> {
220
+ const rows = await this.db
221
+ .selectFrom('credentials')
222
+ .select('userId')
223
+ .where('name', '=', name)
224
+ .where('userId', 'is not', null)
225
+ .execute()
226
+
227
+ return rows.map((row) => row.userId!).filter(Boolean)
228
+ }
229
+
230
+ async getAllUsers(): Promise<string[]> {
231
+ const rows = await this.db
232
+ .selectFrom('credentials')
233
+ .select('userId')
234
+ .distinct()
235
+ .where('userId', 'is not', null)
236
+ .execute()
237
+
238
+ return rows.map((row) => row.userId!).filter(Boolean)
239
+ }
240
+
219
241
  async rotateKEK(): Promise<number> {
220
242
  if (!this.previousKey) {
221
243
  throw new Error('No previousKey configured — nothing to rotate from')
@@ -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,7 +14,7 @@ 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
20
 
@@ -120,7 +120,7 @@ export interface AIRunTable {
120
120
  resourceId: string
121
121
  status: Generated<'running' | 'suspended' | 'completed' | 'failed'>
122
122
  errorMessage: string | null
123
- suspendReason: 'approval' | 'rpc-missing' | null
123
+ suspendReason: 'approval' | 'credential' | 'rpc-missing' | null
124
124
  missingRpcs: string | null
125
125
  usageInputTokens: Generated<number>
126
126
  usageOutputTokens: Generated<number>
@@ -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> {}