@pikku/kysely 0.12.8 → 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,25 @@
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
+
3
23
  ## 0.12.8
4
24
 
5
25
  ### 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
+ }
@@ -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 {
@@ -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.8",
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.14"
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
+ }
@@ -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
 
@@ -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> {}