@pikku/kysely 0.13.12 → 0.13.13
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 +44 -0
- package/dist/src/kysely-ai-run-state-service.d.ts +1 -1
- package/dist/src/kysely-ai-run-state-service.js +8 -3
- package/dist/src/kysely-ai-storage-service.d.ts +1 -1
- package/dist/src/kysely-ai-storage-service.js +5 -2
- package/dist/src/kysely-workflow-service.js +5 -0
- package/package.json +2 -2
- package/src/kysely-ai-run-state-service.ts +9 -4
- package/src/kysely-ai-storage-service.ts +6 -3
- package/src/kysely-webhook-service.test.ts +1 -1
- package/src/kysely-workflow-service.ts +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
## 0.13.13
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 6dada45: fix(workflow,ai-agent): make a run's owner, entry node and step function authoritative
|
|
6
|
+
|
|
7
|
+
A graph run may only start at a node the graph declared in `meta.entryNodeIds`, and
|
|
8
|
+
the generated `POST /workflow/:workflowName/graph/:nodeId` route that let an HTTP
|
|
9
|
+
caller pick the entry node is gone. `startNode` stays for `PikkuTriggerService`,
|
|
10
|
+
which names a declared entry node anyway.
|
|
11
|
+
|
|
12
|
+
`StepState` now records the `rpcName` the workflow dispatched a step with, and the
|
|
13
|
+
step claim rejects a queue message naming a different function with
|
|
14
|
+
`WorkflowStepFunctionMismatchError` before mutating any status — a step runs under
|
|
15
|
+
the run owner's identity and without the `expose` gate, so the message must not
|
|
16
|
+
choose what runs.
|
|
17
|
+
|
|
18
|
+
`approveStep` takes the caller's session, and the generated status routes and
|
|
19
|
+
streams assert the same `assertWorkflowRunOwner` check: a run started through a
|
|
20
|
+
session may only be read and approved by that session's user. A run with no
|
|
21
|
+
recorded owner (trigger, scheduler, unauthenticated route) has nobody to compare
|
|
22
|
+
against and is still gated by the entrypoint's own `auth`/`permissions`.
|
|
23
|
+
|
|
24
|
+
`AIRunStateService.resolveApproval` is now a compare-and-swap returning whether
|
|
25
|
+
_this_ caller made the claim, and both agent resume paths run a tool only for the
|
|
26
|
+
approvals they claimed — concurrent approvals of one tool call no longer all
|
|
27
|
+
execute it.
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [41c1a95]
|
|
30
|
+
- Updated dependencies [ce96383]
|
|
31
|
+
- Updated dependencies [7e60867]
|
|
32
|
+
- Updated dependencies [f8f1244]
|
|
33
|
+
- Updated dependencies [dcf20cb]
|
|
34
|
+
- Updated dependencies [6512384]
|
|
35
|
+
- Updated dependencies [e3b4c14]
|
|
36
|
+
- Updated dependencies [efd0ed1]
|
|
37
|
+
- Updated dependencies [cba98fb]
|
|
38
|
+
- Updated dependencies [ce96383]
|
|
39
|
+
- Updated dependencies [f8f1244]
|
|
40
|
+
- Updated dependencies [f8f1244]
|
|
41
|
+
- Updated dependencies [6e93a35]
|
|
42
|
+
- Updated dependencies [6dada45]
|
|
43
|
+
- @pikku/core@0.12.80
|
|
44
|
+
|
|
1
45
|
## 0.13.12
|
|
2
46
|
|
|
3
47
|
### Patch Changes
|
|
@@ -11,7 +11,7 @@ export declare class KyselyAIRunStateService implements AIRunStateService {
|
|
|
11
11
|
updateRun(runId: string, updates: Partial<AgentRunState>): Promise<void>;
|
|
12
12
|
getRun(runId: string): Promise<AgentRunState | null>;
|
|
13
13
|
getRunsByThread(threadId: string): Promise<AgentRunState[]>;
|
|
14
|
-
resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<
|
|
14
|
+
resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<boolean>;
|
|
15
15
|
findRunByToolCallId(toolCallId: string): Promise<{
|
|
16
16
|
run: AgentRunState;
|
|
17
17
|
approval: PendingApproval;
|
|
@@ -100,14 +100,19 @@ export class KyselyAIRunStateService {
|
|
|
100
100
|
if (filtered.length === 0) {
|
|
101
101
|
updates.status = status;
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
// Compare-and-swap on the list we read: a concurrent approver that got
|
|
104
|
+
// there first has already rewritten it, and this update matches no row.
|
|
105
|
+
const result = await this.db
|
|
104
106
|
.updateTable('aiRun')
|
|
105
107
|
.set(updates)
|
|
106
108
|
.where('runId', '=', row.runId)
|
|
107
|
-
.
|
|
108
|
-
|
|
109
|
+
.where('status', '=', 'suspended')
|
|
110
|
+
.where('pendingApprovals', '=', row.pendingApprovals)
|
|
111
|
+
.executeTakeFirst();
|
|
112
|
+
return Number(result?.numUpdatedRows ?? 0) > 0;
|
|
109
113
|
}
|
|
110
114
|
}
|
|
115
|
+
return false;
|
|
111
116
|
}
|
|
112
117
|
async findRunByToolCallId(toolCallId) {
|
|
113
118
|
const rows = await this.db
|
|
@@ -32,6 +32,6 @@ export declare class KyselyAIStorageService implements AIStorageService, AIRunSt
|
|
|
32
32
|
run: AgentRunState;
|
|
33
33
|
approval: NonNullable<AgentRunState['pendingApprovals']>[number];
|
|
34
34
|
} | null>;
|
|
35
|
-
resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<
|
|
35
|
+
resolveApproval(toolCallId: string, status: 'approved' | 'denied'): Promise<boolean>;
|
|
36
36
|
close(): Promise<void>;
|
|
37
37
|
}
|
|
@@ -510,11 +510,14 @@ export class KyselyAIStorageService {
|
|
|
510
510
|
return { run, approval };
|
|
511
511
|
}
|
|
512
512
|
async resolveApproval(toolCallId, status) {
|
|
513
|
-
|
|
513
|
+
// Only the caller that moves the tool call off 'pending' has claimed it.
|
|
514
|
+
const result = await this.db
|
|
514
515
|
.updateTable('aiToolCall')
|
|
515
516
|
.set({ approvalStatus: status })
|
|
516
517
|
.where('id', '=', toolCallId)
|
|
517
|
-
.
|
|
518
|
+
.where('approvalStatus', '=', 'pending')
|
|
519
|
+
.executeTakeFirst();
|
|
520
|
+
return Number(result?.numUpdatedRows ?? 0) > 0;
|
|
518
521
|
}
|
|
519
522
|
async close() { }
|
|
520
523
|
}
|
|
@@ -78,6 +78,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
78
78
|
return {
|
|
79
79
|
stepId,
|
|
80
80
|
status: 'pending',
|
|
81
|
+
rpcName,
|
|
81
82
|
result: undefined,
|
|
82
83
|
error: undefined,
|
|
83
84
|
attemptCount: 1,
|
|
@@ -94,6 +95,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
94
95
|
.select([
|
|
95
96
|
'workflowStepId',
|
|
96
97
|
'status',
|
|
98
|
+
'rpcName',
|
|
97
99
|
'result',
|
|
98
100
|
'error',
|
|
99
101
|
'retries',
|
|
@@ -115,6 +117,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
115
117
|
return {
|
|
116
118
|
stepId: row.workflowStepId,
|
|
117
119
|
status: row.status,
|
|
120
|
+
rpcName: row.rpcName ?? null,
|
|
118
121
|
result: parseJson(row.result),
|
|
119
122
|
error: parseJson(row.error),
|
|
120
123
|
attemptCount: Number(row.currentAttempt ?? 1),
|
|
@@ -331,6 +334,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
331
334
|
.select([
|
|
332
335
|
'workflowStepId',
|
|
333
336
|
'status',
|
|
337
|
+
'rpcName',
|
|
334
338
|
'result',
|
|
335
339
|
'error',
|
|
336
340
|
'retries',
|
|
@@ -345,6 +349,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
345
349
|
return {
|
|
346
350
|
stepId: row.workflowStepId,
|
|
347
351
|
status: row.status,
|
|
352
|
+
rpcName: row.rpcName ?? null,
|
|
348
353
|
result: parseJson(row.result),
|
|
349
354
|
error: parseJson(row.error),
|
|
350
355
|
attemptCount: Number(row.currentAttempt ?? 1),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/kysely",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.13",
|
|
4
4
|
"description": "Kysely-backed Pikku services — workflow state, secrets, sessions, scopes and audit",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"prepublishOnly": "yarn build"
|
|
22
22
|
},
|
|
23
23
|
"peerDependencies": {
|
|
24
|
-
"@pikku/core": "^0.12.
|
|
24
|
+
"@pikku/core": "^0.12.80"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"kysely": "^0.29.0"
|
|
@@ -89,7 +89,7 @@ export class KyselyAIRunStateService implements AIRunStateService {
|
|
|
89
89
|
async resolveApproval(
|
|
90
90
|
toolCallId: string,
|
|
91
91
|
status: 'approved' | 'denied'
|
|
92
|
-
): Promise<
|
|
92
|
+
): Promise<boolean> {
|
|
93
93
|
const rows = await this.db
|
|
94
94
|
.selectFrom('aiRun')
|
|
95
95
|
.select(['runId', 'pendingApprovals'])
|
|
@@ -117,14 +117,19 @@ export class KyselyAIRunStateService implements AIRunStateService {
|
|
|
117
117
|
if (filtered.length === 0) {
|
|
118
118
|
updates.status = status
|
|
119
119
|
}
|
|
120
|
-
|
|
120
|
+
// Compare-and-swap on the list we read: a concurrent approver that got
|
|
121
|
+
// there first has already rewritten it, and this update matches no row.
|
|
122
|
+
const result = await this.db
|
|
121
123
|
.updateTable('aiRun')
|
|
122
124
|
.set(updates as any)
|
|
123
125
|
.where('runId', '=', row.runId)
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
+
.where('status', '=', 'suspended')
|
|
127
|
+
.where('pendingApprovals', '=', row.pendingApprovals)
|
|
128
|
+
.executeTakeFirst()
|
|
129
|
+
return Number(result?.numUpdatedRows ?? 0) > 0
|
|
126
130
|
}
|
|
127
131
|
}
|
|
132
|
+
return false
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
async findRunByToolCallId(
|
|
@@ -596,12 +596,15 @@ export class KyselyAIStorageService
|
|
|
596
596
|
async resolveApproval(
|
|
597
597
|
toolCallId: string,
|
|
598
598
|
status: 'approved' | 'denied'
|
|
599
|
-
): Promise<
|
|
600
|
-
|
|
599
|
+
): Promise<boolean> {
|
|
600
|
+
// Only the caller that moves the tool call off 'pending' has claimed it.
|
|
601
|
+
const result = await this.db
|
|
601
602
|
.updateTable('aiToolCall')
|
|
602
603
|
.set({ approvalStatus: status })
|
|
603
604
|
.where('id', '=', toolCallId)
|
|
604
|
-
.
|
|
605
|
+
.where('approvalStatus', '=', 'pending')
|
|
606
|
+
.executeTakeFirst()
|
|
607
|
+
return Number(result?.numUpdatedRows ?? 0) > 0
|
|
605
608
|
}
|
|
606
609
|
|
|
607
610
|
public async close(): Promise<void> {}
|
|
@@ -2,7 +2,7 @@ import { describe, test, beforeEach } from 'node:test'
|
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
3
|
import { CamelCasePlugin, Kysely, SqliteDialect } from 'kysely'
|
|
4
4
|
import Database from 'better-sqlite3'
|
|
5
|
-
import { setSingletonServices } from '@pikku/core'
|
|
5
|
+
import { setSingletonServices } from '@pikku/core/ecosystem'
|
|
6
6
|
|
|
7
7
|
import type { KyselyPikkuDB } from './kysely-tables.js'
|
|
8
8
|
import { SerializePlugin } from './serialize-plugin.js'
|
|
@@ -123,6 +123,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
123
123
|
return {
|
|
124
124
|
stepId,
|
|
125
125
|
status: 'pending',
|
|
126
|
+
rpcName,
|
|
126
127
|
result: undefined,
|
|
127
128
|
error: undefined,
|
|
128
129
|
attemptCount: 1,
|
|
@@ -140,6 +141,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
140
141
|
.select([
|
|
141
142
|
'workflowStepId',
|
|
142
143
|
'status',
|
|
144
|
+
'rpcName',
|
|
143
145
|
'result',
|
|
144
146
|
'error',
|
|
145
147
|
'retries',
|
|
@@ -165,6 +167,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
165
167
|
return {
|
|
166
168
|
stepId: row.workflowStepId,
|
|
167
169
|
status: row.status as StepState['status'],
|
|
170
|
+
rpcName: row.rpcName ?? null,
|
|
168
171
|
result: parseJson(row.result),
|
|
169
172
|
error: parseJson(row.error),
|
|
170
173
|
attemptCount: Number(row.currentAttempt ?? 1),
|
|
@@ -425,6 +428,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
425
428
|
.select([
|
|
426
429
|
'workflowStepId',
|
|
427
430
|
'status',
|
|
431
|
+
'rpcName',
|
|
428
432
|
'result',
|
|
429
433
|
'error',
|
|
430
434
|
'retries',
|
|
@@ -440,6 +444,7 @@ export class KyselyWorkflowService extends PikkuWorkflowService {
|
|
|
440
444
|
return {
|
|
441
445
|
stepId: row.workflowStepId,
|
|
442
446
|
status: row.status as StepState['status'],
|
|
447
|
+
rpcName: row.rpcName ?? null,
|
|
443
448
|
result: parseJson(row.result),
|
|
444
449
|
error: parseJson(row.error),
|
|
445
450
|
attemptCount: Number(row.currentAttempt ?? 1),
|