@pikku/mongodb 0.12.5 → 0.12.7
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 +77 -0
- package/dist/src/mongodb-agent-run-service.d.ts +1 -0
- package/dist/src/mongodb-agent-run-service.js +4 -1
- package/dist/src/mongodb-channel-store.d.ts +3 -0
- package/dist/src/mongodb-channel-store.js +10 -0
- package/dist/src/mongodb-secret-service.d.ts +3 -3
- package/dist/src/mongodb-secret-service.js +18 -7
- package/dist/src/mongodb-workflow-service.d.ts +20 -15
- package/dist/src/mongodb-workflow-service.js +28 -13
- package/package.json +5 -2
- package/src/mongodb-agent-run-service.ts +6 -1
- package/src/mongodb-channel-store.ts +17 -0
- package/src/mongodb-secret-service.ts +26 -17
- package/src/mongodb-services.test.ts +2 -2
- package/src/mongodb-workflow-service.ts +47 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,82 @@
|
|
|
1
1
|
# @pikku/mongodb
|
|
2
2
|
|
|
3
|
+
## 0.12.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 92cd5b1: feat(workflow): workflow-owned step retries + stable invocationId
|
|
8
|
+
|
|
9
|
+
The workflow — not the queue — now owns step retry policy, and each step
|
|
10
|
+
invocation gets a stable idempotency key.
|
|
11
|
+
- **Default `retries: 5` with exponential backoff.** A step with no `retries`
|
|
12
|
+
previously inherited the queue's bare default (e.g. pg-boss `retry_limit 2`,
|
|
13
|
+
no backoff) so retries fired instantly and couldn't outlast a transient
|
|
14
|
+
outage. Retries now default to 5 with backoff, resolved at the workflow layer.
|
|
15
|
+
- **`retries: 0` is honored.** Dispatch previously passed `undefined` options
|
|
16
|
+
for `retries: 0`, letting the queue re-run a non-idempotent step up to its own
|
|
17
|
+
default. The resolved policy now always sets `attempts` (`retries: 0` →
|
|
18
|
+
`attempts: 1`), so the queue never second-guesses the workflow. The persisted
|
|
19
|
+
step retries and the dispatched `attempts` are resolved together so
|
|
20
|
+
"retries exhausted" and "no more redeliveries" are the same event.
|
|
21
|
+
- **`workflowStep.invocationId`** — a deterministic, dependency-free
|
|
22
|
+
`uuidv5(runId:stepName)` handed to every step. Unlike `stepId` (minted per
|
|
23
|
+
attempt), it is identical across retries, so a step can dedupe on it
|
|
24
|
+
(`ON CONFLICT (invocationId)`, Stripe idempotency keys, etc.).
|
|
25
|
+
- **queue-bullmq**: `mapPikkuJobToBull` now maps `backoff` (previously dropped,
|
|
26
|
+
so a step's backoff silently never applied on Redis), and `registerQueues`
|
|
27
|
+
throws a clear error when no logger is available (matching queue-pg-boss).
|
|
28
|
+
- **Dispatch failures are recoverable, not fatal.** A step is now marked
|
|
29
|
+
`scheduled` only _after_ it is successfully handed to its transport (queue or
|
|
30
|
+
scheduler) — a failed hand-off leaves it `pending` so a replay re-dispatches
|
|
31
|
+
it, instead of stranding it in `scheduled` (replay would pause forever on a
|
|
32
|
+
job that was never enqueued). A transport outage (e.g. pg-boss momentarily
|
|
33
|
+
down) is surfaced as a new `WorkflowDispatchException`, which the orchestrator
|
|
34
|
+
treats as transient: the run is left running and the orchestrator job is
|
|
35
|
+
rethrown for redelivery (it replays idempotently from the snapshot) rather
|
|
36
|
+
than the whole run being marked `failed`. The orchestrator job now also
|
|
37
|
+
carries its own retry policy, so this holds even when the orchestrator queue
|
|
38
|
+
is configured `retry_limit 0`. A genuine step error still fails the run.
|
|
39
|
+
- **Same step name can be invoked multiple times in one run.** Step rows are now
|
|
40
|
+
keyed per _invocation_: the Nth reach of a step name in a replay resolves to a
|
|
41
|
+
physical key (`name` for the first, `name#N` for repeats), so a literal
|
|
42
|
+
duplicate name no longer clobbers the earlier step's state. The first reach
|
|
43
|
+
keeps the bare name, so existing rows, graph-node matching and `invocationId`s
|
|
44
|
+
are unchanged. Ordinals are derived deterministically from DSL execution order
|
|
45
|
+
and reset each replay.
|
|
46
|
+
- **Step provenance (`fromStepName`) + graph cycles.** Every step now records
|
|
47
|
+
the predecessor it was scheduled from (`fromStepName`; entry steps have none),
|
|
48
|
+
persisted on the step row across all stores (in-memory, kysely, redis,
|
|
49
|
+
mongodb, cloudflare DO) and carried in the queued payload. The DSL wire
|
|
50
|
+
exposes the derived `fromInvocationId` (`uuidv5(runId:fromStepName)`) so
|
|
51
|
+
consumers get the stable predecessor key without a second persisted id —
|
|
52
|
+
`fromStepName` is the source of truth (it is replay-deterministic; `stepId`,
|
|
53
|
+
minted per row, is not). This makes the walked path reconstructable even when
|
|
54
|
+
a node is reached more than once: in `a → b → a → c` the second `a` is a
|
|
55
|
+
distinct ordinal instance (`a#1`) whose `fromStepName` is `b`.
|
|
56
|
+
The graph runner now supports **cycles**: a forward edge into an
|
|
57
|
+
already-started node still collapses to a single run (joins/diamonds are
|
|
58
|
+
unchanged), but a _back-edge_ — one whose target can reach its source — fires
|
|
59
|
+
a fresh ordinal instance, so a node can loop back to itself. Termination is
|
|
60
|
+
the graph's responsibility (branch routing must converge); the engine enforces
|
|
61
|
+
no visit cap.
|
|
62
|
+
|
|
63
|
+
- Updated dependencies [92cd5b1]
|
|
64
|
+
- @pikku/core@0.12.38
|
|
65
|
+
|
|
66
|
+
## 0.12.6
|
|
67
|
+
|
|
68
|
+
### Patch Changes
|
|
69
|
+
|
|
70
|
+
- 4b5c75b: feat(auth-js): wire OIDC config (issuer/tenantId) as variables, expand provider registry
|
|
71
|
+
- Move `issuer` and `tenantId` out of the secret blob for OIDC providers (auth0, okta, azure-ad, keycloak, cognito, microsoft-entra-id) — they are public config URLs, not secrets. Now registered via `wireVariable` and loaded at runtime via `services.variables.get()`.
|
|
72
|
+
- Expand provider registry from 13 to 31 providers: reddit, notion, instagram, zoom, figma, tiktok, threads, patreon, dropbox, bitbucket, hubspot, salesforce, atlassian, strava, keycloak, cognito, microsoft-entra-id added.
|
|
73
|
+
- `serialize-auth-gen` emits `wireVariable({...})` declarations and `services.variables.get()` calls in the generated factory for OIDC providers.
|
|
74
|
+
- Integration verifier exercises real `/auth/providers` endpoint with `LocalSecretService` + `LocalVariablesService`, including a spy test proving `services.variables.get('AUTH0_ISSUER')` is called at request time.
|
|
75
|
+
|
|
76
|
+
- Updated dependencies [4b5c75b]
|
|
77
|
+
- Updated dependencies [4b5c75b]
|
|
78
|
+
- @pikku/core@0.12.27
|
|
79
|
+
|
|
3
80
|
## 0.12.5
|
|
4
81
|
|
|
5
82
|
### Patch Changes
|
|
@@ -10,8 +10,11 @@ export class MongoDBAgentRunService {
|
|
|
10
10
|
this.runs = db.collection('ai_run');
|
|
11
11
|
}
|
|
12
12
|
async listThreads(options) {
|
|
13
|
-
const { agentName, limit = 50, offset = 0 } = options ?? {};
|
|
13
|
+
const { agentName, resourceId, limit = 50, offset = 0 } = options ?? {};
|
|
14
14
|
let filter = {};
|
|
15
|
+
if (resourceId) {
|
|
16
|
+
filter.resourceId = resourceId;
|
|
17
|
+
}
|
|
15
18
|
if (agentName) {
|
|
16
19
|
const threadIds = await this.runs.distinct('threadId', {
|
|
17
20
|
agentName,
|
|
@@ -14,5 +14,8 @@ export declare class MongoDBChannelStore extends ChannelStore {
|
|
|
14
14
|
getChannel(channelId: string): Promise<Channel & {
|
|
15
15
|
pikkuUserId?: string;
|
|
16
16
|
}>;
|
|
17
|
+
setState(channelId: string, state: unknown): Promise<void>;
|
|
18
|
+
getState(channelId: string): Promise<unknown | undefined>;
|
|
19
|
+
clearState(channelId: string): Promise<void>;
|
|
17
20
|
close(): Promise<void>;
|
|
18
21
|
}
|
|
@@ -48,5 +48,15 @@ export class MongoDBChannelStore extends ChannelStore {
|
|
|
48
48
|
pikkuUserId: row.pikkuUserId ?? undefined,
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
+
async setState(channelId, state) {
|
|
52
|
+
await this.channels.updateOne({ _id: channelId }, { $set: { state } });
|
|
53
|
+
}
|
|
54
|
+
async getState(channelId) {
|
|
55
|
+
const row = await this.channels.findOne({ _id: channelId }, { projection: { state: 1 } });
|
|
56
|
+
return row?.state ?? undefined;
|
|
57
|
+
}
|
|
58
|
+
async clearState(channelId) {
|
|
59
|
+
await this.channels.updateOne({ _id: channelId }, { $unset: { state: '' } });
|
|
60
|
+
}
|
|
51
61
|
async close() { }
|
|
52
62
|
}
|
|
@@ -21,10 +21,10 @@ export declare class MongoDBSecretService implements SecretService {
|
|
|
21
21
|
init(): Promise<void>;
|
|
22
22
|
private logAudit;
|
|
23
23
|
private getKEK;
|
|
24
|
-
getSecret(key: string): Promise<
|
|
25
|
-
getSecretJSON<R = {}>(key: string): Promise<R>;
|
|
24
|
+
getSecret<T = string>(key: string): Promise<T>;
|
|
26
25
|
hasSecret(key: string): Promise<boolean>;
|
|
27
|
-
|
|
26
|
+
setSecret(key: string, value: unknown): Promise<void>;
|
|
28
27
|
deleteSecret(key: string): Promise<void>;
|
|
28
|
+
getSecrets<T extends Record<string, unknown> = Record<string, unknown>>(keys: (keyof T & string)[]): Promise<T>;
|
|
29
29
|
rotateKEK(): Promise<number>;
|
|
30
30
|
}
|
|
@@ -56,17 +56,12 @@ export class MongoDBSecretService {
|
|
|
56
56
|
await this.logAudit(key, 'read');
|
|
57
57
|
return result;
|
|
58
58
|
}
|
|
59
|
-
async getSecretJSON(key) {
|
|
60
|
-
const raw = await this.getSecret(key);
|
|
61
|
-
return JSON.parse(raw);
|
|
62
|
-
}
|
|
63
59
|
async hasSecret(key) {
|
|
64
60
|
const count = await this.secrets.countDocuments({ _id: key }, { limit: 1 });
|
|
65
61
|
return count > 0;
|
|
66
62
|
}
|
|
67
|
-
async
|
|
68
|
-
const
|
|
69
|
-
const { ciphertext, wrappedDEK } = await envelopeEncrypt(this.key, plaintext);
|
|
63
|
+
async setSecret(key, value) {
|
|
64
|
+
const { ciphertext, wrappedDEK } = await envelopeEncrypt(this.key, value);
|
|
70
65
|
const now = new Date();
|
|
71
66
|
await this.secrets.updateOne({ _id: key }, {
|
|
72
67
|
$set: {
|
|
@@ -86,6 +81,22 @@ export class MongoDBSecretService {
|
|
|
86
81
|
await this.secrets.deleteOne({ _id: key });
|
|
87
82
|
await this.logAudit(key, 'delete');
|
|
88
83
|
}
|
|
84
|
+
async getSecrets(keys) {
|
|
85
|
+
const rows = await this.secrets
|
|
86
|
+
.find({ _id: { $in: keys } })
|
|
87
|
+
.toArray();
|
|
88
|
+
const out = {};
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
try {
|
|
91
|
+
const kek = this.getKEK(row.keyVersion);
|
|
92
|
+
out[row._id] = await envelopeDecrypt(kek, row.ciphertext, row.wrappedDek);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// skip secrets that fail to decrypt
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
89
100
|
async rotateKEK() {
|
|
90
101
|
if (!this.previousKey) {
|
|
91
102
|
throw new Error('No previousKey configured — nothing to rotate from');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SerializedError } from '@pikku/core';
|
|
2
|
-
import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
|
|
2
|
+
import { PikkuWorkflowService, type WorkflowPlannedStep, type WorkflowRun, type WorkflowRunWire, type StepState, type StepStatus, type WorkflowStatus, type WorkflowVersionStatus } from '@pikku/core/workflow';
|
|
3
3
|
import type { Db } from 'mongodb';
|
|
4
4
|
export declare class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
5
5
|
private initialized;
|
|
@@ -10,28 +10,28 @@ export declare class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
10
10
|
private versions;
|
|
11
11
|
constructor(db: Db);
|
|
12
12
|
init(): Promise<void>;
|
|
13
|
-
|
|
13
|
+
protected createRunImpl(workflowName: string, input: any, inline: boolean, graphHash: string, wire: WorkflowRunWire, options?: {
|
|
14
14
|
deterministic?: boolean;
|
|
15
15
|
plannedSteps?: WorkflowPlannedStep[];
|
|
16
16
|
}): Promise<string>;
|
|
17
17
|
getRun(id: string): Promise<WorkflowRun | null>;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
protected updateRunStatusImpl(id: string, status: WorkflowStatus, output?: any, error?: SerializedError): Promise<void>;
|
|
19
|
+
protected insertStepStateImpl(runId: string, stepName: string, rpcName: string | null, data: any, stepOptions?: {
|
|
20
20
|
retries?: number;
|
|
21
21
|
retryDelay?: string | number;
|
|
22
|
-
}): Promise<StepState>;
|
|
22
|
+
}, fromStepName?: string): Promise<StepState>;
|
|
23
23
|
getStepState(runId: string, stepName: string): Promise<StepState>;
|
|
24
24
|
getRunHistory(runId: string): Promise<Array<StepState & {
|
|
25
25
|
stepName: string;
|
|
26
26
|
}>>;
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
protected setStepRunningImpl(stepId: string): Promise<void>;
|
|
28
|
+
protected setStepScheduledImpl(stepId: string): Promise<void>;
|
|
29
29
|
private insertHistoryRecord;
|
|
30
30
|
private getTimestampFieldForStatus;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
protected setStepChildRunIdImpl(stepId: string, childRunId: string): Promise<void>;
|
|
32
|
+
protected setStepResultImpl(stepId: string, result: any): Promise<void>;
|
|
33
|
+
protected setStepErrorImpl(stepId: string, error: Error): Promise<void>;
|
|
34
|
+
protected createRetryAttemptImpl(stepId: string, status: 'pending' | 'running'): Promise<StepState>;
|
|
35
35
|
withRunLock<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
36
36
|
withStepLock<T>(_runId: string, _stepName: string, fn: () => Promise<T>): Promise<T>;
|
|
37
37
|
getCompletedGraphState(runId: string): Promise<{
|
|
@@ -40,12 +40,17 @@ export declare class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
40
40
|
branchKeys: Record<string, string>;
|
|
41
41
|
}>;
|
|
42
42
|
getNodesWithoutSteps(runId: string, nodeIds: string[]): Promise<string[]>;
|
|
43
|
+
getStepInstances(runId: string): Promise<Array<{
|
|
44
|
+
stepName: string;
|
|
45
|
+
status: StepStatus;
|
|
46
|
+
fromStepName?: string;
|
|
47
|
+
}>>;
|
|
43
48
|
getNodeResults(runId: string, nodeIds: string[]): Promise<Record<string, any>>;
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
protected setBranchTakenImpl(stepId: string, branchKey: string): Promise<void>;
|
|
50
|
+
protected updateRunStateImpl(runId: string, name: string, value: unknown): Promise<void>;
|
|
46
51
|
getRunState(runId: string): Promise<Record<string, unknown>>;
|
|
47
|
-
|
|
48
|
-
|
|
52
|
+
protected upsertWorkflowVersionImpl(name: string, graphHash: string, graph: any, source: string, status?: WorkflowVersionStatus): Promise<void>;
|
|
53
|
+
protected updateWorkflowVersionStatusImpl(name: string, graphHash: string, status: WorkflowVersionStatus): Promise<void>;
|
|
49
54
|
getWorkflowVersion(name: string, graphHash: string): Promise<{
|
|
50
55
|
graph: any;
|
|
51
56
|
source: string;
|
|
@@ -28,7 +28,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
28
28
|
await this.versions.createIndex({ workflowName: 1, graphHash: 1 }, { unique: true });
|
|
29
29
|
this.initialized = true;
|
|
30
30
|
}
|
|
31
|
-
async
|
|
31
|
+
async createRunImpl(workflowName, input, inline, graphHash, wire, options) {
|
|
32
32
|
const id = crypto.randomUUID();
|
|
33
33
|
const now = new Date();
|
|
34
34
|
await this.runs.insertOne({
|
|
@@ -52,7 +52,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
52
52
|
async getRun(id) {
|
|
53
53
|
return this.runService.getRun(id);
|
|
54
54
|
}
|
|
55
|
-
async
|
|
55
|
+
async updateRunStatusImpl(id, status, output, error) {
|
|
56
56
|
await this.runs.updateOne({ _id: id }, {
|
|
57
57
|
$set: {
|
|
58
58
|
status,
|
|
@@ -62,7 +62,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
62
62
|
},
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
-
async
|
|
65
|
+
async insertStepStateImpl(runId, stepName, rpcName, data, stepOptions, fromStepName) {
|
|
66
66
|
const stepId = crypto.randomUUID();
|
|
67
67
|
const now = new Date();
|
|
68
68
|
await this.steps.insertOne({
|
|
@@ -77,6 +77,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
77
77
|
branchTaken: null,
|
|
78
78
|
retries: stepOptions?.retries ?? null,
|
|
79
79
|
retryDelay: stepOptions?.retryDelay?.toString() ?? null,
|
|
80
|
+
fromStepName: fromStepName ?? null,
|
|
80
81
|
createdAt: now,
|
|
81
82
|
updatedAt: now,
|
|
82
83
|
});
|
|
@@ -89,6 +90,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
89
90
|
attemptCount: 1,
|
|
90
91
|
retries: stepOptions?.retries,
|
|
91
92
|
retryDelay: stepOptions?.retryDelay?.toString(),
|
|
93
|
+
fromStepName,
|
|
92
94
|
createdAt: now,
|
|
93
95
|
updatedAt: now,
|
|
94
96
|
};
|
|
@@ -112,6 +114,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
112
114
|
attemptCount,
|
|
113
115
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
114
116
|
retryDelay: row.retryDelay ?? undefined,
|
|
117
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
115
118
|
createdAt: new Date(row.createdAt),
|
|
116
119
|
updatedAt: new Date(row.updatedAt),
|
|
117
120
|
};
|
|
@@ -119,7 +122,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
119
122
|
async getRunHistory(runId) {
|
|
120
123
|
return this.runService.getRunHistory(runId);
|
|
121
124
|
}
|
|
122
|
-
async
|
|
125
|
+
async setStepRunningImpl(stepId) {
|
|
123
126
|
await this.steps.updateOne({ _id: stepId }, { $set: { status: 'running', updatedAt: new Date() } });
|
|
124
127
|
const latestHistory = await this.stepHistory
|
|
125
128
|
.find({ workflowStepId: stepId })
|
|
@@ -130,7 +133,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
130
133
|
await this.stepHistory.updateOne({ _id: latestHistory[0]._id }, { $set: { status: 'running' } });
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
|
-
async
|
|
136
|
+
async setStepScheduledImpl(stepId) {
|
|
134
137
|
await this.steps.updateOne({ _id: stepId }, { $set: { status: 'scheduled', updatedAt: new Date() } });
|
|
135
138
|
}
|
|
136
139
|
async insertHistoryRecord(stepId, status, result, error) {
|
|
@@ -167,7 +170,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
167
170
|
return 'createdAt';
|
|
168
171
|
}
|
|
169
172
|
}
|
|
170
|
-
async
|
|
173
|
+
async setStepChildRunIdImpl(stepId, childRunId) {
|
|
171
174
|
await this.steps.updateOne({ _id: stepId }, {
|
|
172
175
|
$set: {
|
|
173
176
|
childRunId,
|
|
@@ -175,7 +178,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
175
178
|
},
|
|
176
179
|
});
|
|
177
180
|
}
|
|
178
|
-
async
|
|
181
|
+
async setStepResultImpl(stepId, result) {
|
|
179
182
|
await this.steps.updateOne({ _id: stepId }, {
|
|
180
183
|
$set: {
|
|
181
184
|
status: 'succeeded',
|
|
@@ -193,7 +196,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
193
196
|
await this.stepHistory.updateOne({ _id: latestHistory[0]._id }, { $set: { status: 'succeeded', result } });
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
|
-
async
|
|
199
|
+
async setStepErrorImpl(stepId, error) {
|
|
197
200
|
const serializedError = {
|
|
198
201
|
message: error.message,
|
|
199
202
|
stack: error.stack,
|
|
@@ -216,7 +219,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
216
219
|
await this.stepHistory.updateOne({ _id: latestHistory[0]._id }, { $set: { status: 'failed', error: serializedError } });
|
|
217
220
|
}
|
|
218
221
|
}
|
|
219
|
-
async
|
|
222
|
+
async createRetryAttemptImpl(stepId, status) {
|
|
220
223
|
await this.steps.updateOne({ _id: stepId }, {
|
|
221
224
|
$set: {
|
|
222
225
|
status,
|
|
@@ -240,6 +243,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
240
243
|
attemptCount,
|
|
241
244
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
242
245
|
retryDelay: row.retryDelay ?? undefined,
|
|
246
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
243
247
|
createdAt: new Date(row.createdAt),
|
|
244
248
|
updatedAt: new Date(row.updatedAt),
|
|
245
249
|
};
|
|
@@ -293,6 +297,17 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
293
297
|
const existingStepNames = new Set(result.map((r) => r.stepName));
|
|
294
298
|
return nodeIds.filter((id) => !existingStepNames.has(id));
|
|
295
299
|
}
|
|
300
|
+
async getStepInstances(runId) {
|
|
301
|
+
const rows = await this.steps
|
|
302
|
+
.find({ workflowRunId: runId })
|
|
303
|
+
.project({ stepName: 1, status: 1, fromStepName: 1 })
|
|
304
|
+
.toArray();
|
|
305
|
+
return rows.map((r) => ({
|
|
306
|
+
stepName: r.stepName,
|
|
307
|
+
status: r.status,
|
|
308
|
+
fromStepName: r.fromStepName ?? undefined,
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
296
311
|
async getNodeResults(runId, nodeIds) {
|
|
297
312
|
if (nodeIds.length === 0)
|
|
298
313
|
return {};
|
|
@@ -309,10 +324,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
309
324
|
}
|
|
310
325
|
return results;
|
|
311
326
|
}
|
|
312
|
-
async
|
|
327
|
+
async setBranchTakenImpl(stepId, branchKey) {
|
|
313
328
|
await this.steps.updateOne({ _id: stepId }, { $set: { branchTaken: branchKey, updatedAt: new Date() } });
|
|
314
329
|
}
|
|
315
|
-
async
|
|
330
|
+
async updateRunStateImpl(runId, name, value) {
|
|
316
331
|
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
|
|
317
332
|
throw new Error('Invalid state key name');
|
|
318
333
|
}
|
|
@@ -329,7 +344,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
329
344
|
return {};
|
|
330
345
|
return row.state ?? {};
|
|
331
346
|
}
|
|
332
|
-
async
|
|
347
|
+
async upsertWorkflowVersionImpl(name, graphHash, graph, source, status) {
|
|
333
348
|
await this.versions.updateOne({ workflowName: name, graphHash }, {
|
|
334
349
|
$setOnInsert: {
|
|
335
350
|
workflowName: name,
|
|
@@ -341,7 +356,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
341
356
|
},
|
|
342
357
|
}, { upsert: true });
|
|
343
358
|
}
|
|
344
|
-
async
|
|
359
|
+
async updateWorkflowVersionStatusImpl(name, graphHash, status) {
|
|
345
360
|
await this.versions.updateOne({ workflowName: name, graphHash }, { $set: { status } });
|
|
346
361
|
}
|
|
347
362
|
async getWorkflowVersion(name, graphHash) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/mongodb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.7",
|
|
4
4
|
"author": "yasser.fadl@gmail.com",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "dist/src/index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"prepublishOnly": "yarn build"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@pikku/core": "^0.12.
|
|
20
|
+
"@pikku/core": "^0.12.38"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"mongodb": "^6.12.0"
|
|
@@ -26,5 +26,8 @@
|
|
|
26
26
|
"mongodb": "^6.12.0",
|
|
27
27
|
"mongodb-memory-server": "^10.4.0",
|
|
28
28
|
"typescript": "^5.9"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
".": "./dist/src/index.js"
|
|
29
32
|
}
|
|
30
33
|
}
|
|
@@ -70,13 +70,18 @@ export class MongoDBAgentRunService implements AgentRunService {
|
|
|
70
70
|
|
|
71
71
|
async listThreads(options?: {
|
|
72
72
|
agentName?: string
|
|
73
|
+
resourceId?: string
|
|
73
74
|
limit?: number
|
|
74
75
|
offset?: number
|
|
75
76
|
}): Promise<AIThread[]> {
|
|
76
|
-
const { agentName, limit = 50, offset = 0 } = options ?? {}
|
|
77
|
+
const { agentName, resourceId, limit = 50, offset = 0 } = options ?? {}
|
|
77
78
|
|
|
78
79
|
let filter: Record<string, any> = {}
|
|
79
80
|
|
|
81
|
+
if (resourceId) {
|
|
82
|
+
filter.resourceId = resourceId
|
|
83
|
+
}
|
|
84
|
+
|
|
80
85
|
if (agentName) {
|
|
81
86
|
const threadIds = await this.runs.distinct('threadId', {
|
|
82
87
|
agentName,
|
|
@@ -8,6 +8,7 @@ interface ChannelDoc {
|
|
|
8
8
|
createdAt: Date
|
|
9
9
|
openingData: any
|
|
10
10
|
pikkuUserId: string | null
|
|
11
|
+
state?: unknown
|
|
11
12
|
lastWire: Date
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -88,5 +89,21 @@ export class MongoDBChannelStore extends ChannelStore {
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
public async setState(channelId: string, state: unknown): Promise<void> {
|
|
93
|
+
await this.channels.updateOne({ _id: channelId }, { $set: { state } })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async getState(channelId: string): Promise<unknown | undefined> {
|
|
97
|
+
const row = await this.channels.findOne(
|
|
98
|
+
{ _id: channelId },
|
|
99
|
+
{ projection: { state: 1 } }
|
|
100
|
+
)
|
|
101
|
+
return row?.state ?? undefined
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public async clearState(channelId: string): Promise<void> {
|
|
105
|
+
await this.channels.updateOne({ _id: channelId }, { $unset: { state: '' } })
|
|
106
|
+
}
|
|
107
|
+
|
|
91
108
|
public async close(): Promise<void> {}
|
|
92
109
|
}
|
|
@@ -87,36 +87,23 @@ export class MongoDBSecretService implements SecretService {
|
|
|
87
87
|
throw new Error(`No KEK available for key_version ${version}`)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
async getSecret(key: string): Promise<
|
|
90
|
+
async getSecret<T = string>(key: string): Promise<T> {
|
|
91
91
|
const row = await this.secrets.findOne({ _id: key })
|
|
92
92
|
if (!row) throw new Error('Requested secret not found')
|
|
93
93
|
|
|
94
94
|
const kek = this.getKEK(row.keyVersion)
|
|
95
|
-
const result = await envelopeDecrypt<
|
|
96
|
-
kek,
|
|
97
|
-
row.ciphertext,
|
|
98
|
-
row.wrappedDek
|
|
99
|
-
)
|
|
95
|
+
const result = await envelopeDecrypt<T>(kek, row.ciphertext, row.wrappedDek)
|
|
100
96
|
await this.logAudit(key, 'read')
|
|
101
97
|
return result
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
async getSecretJSON<R = {}>(key: string): Promise<R> {
|
|
105
|
-
const raw = await this.getSecret(key)
|
|
106
|
-
return JSON.parse(raw) as R
|
|
107
|
-
}
|
|
108
|
-
|
|
109
100
|
async hasSecret(key: string): Promise<boolean> {
|
|
110
101
|
const count = await this.secrets.countDocuments({ _id: key }, { limit: 1 })
|
|
111
102
|
return count > 0
|
|
112
103
|
}
|
|
113
104
|
|
|
114
|
-
async
|
|
115
|
-
const
|
|
116
|
-
const { ciphertext, wrappedDEK } = await envelopeEncrypt(
|
|
117
|
-
this.key,
|
|
118
|
-
plaintext
|
|
119
|
-
)
|
|
105
|
+
async setSecret(key: string, value: unknown): Promise<void> {
|
|
106
|
+
const { ciphertext, wrappedDEK } = await envelopeEncrypt(this.key, value)
|
|
120
107
|
const now = new Date()
|
|
121
108
|
|
|
122
109
|
await this.secrets.updateOne(
|
|
@@ -144,6 +131,28 @@ export class MongoDBSecretService implements SecretService {
|
|
|
144
131
|
await this.logAudit(key, 'delete')
|
|
145
132
|
}
|
|
146
133
|
|
|
134
|
+
async getSecrets<
|
|
135
|
+
T extends Record<string, unknown> = Record<string, unknown>,
|
|
136
|
+
>(keys: (keyof T & string)[]): Promise<T> {
|
|
137
|
+
const rows = await this.secrets
|
|
138
|
+
.find({ _id: { $in: keys } } as any)
|
|
139
|
+
.toArray()
|
|
140
|
+
const out: Record<string, unknown> = {}
|
|
141
|
+
for (const row of rows) {
|
|
142
|
+
try {
|
|
143
|
+
const kek = this.getKEK(row.keyVersion)
|
|
144
|
+
out[row._id] = await envelopeDecrypt(
|
|
145
|
+
kek,
|
|
146
|
+
row.ciphertext,
|
|
147
|
+
row.wrappedDek
|
|
148
|
+
)
|
|
149
|
+
} catch {
|
|
150
|
+
// skip secrets that fail to decrypt
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out as T
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
async rotateKEK(): Promise<number> {
|
|
148
157
|
if (!this.previousKey) {
|
|
149
158
|
throw new Error('No previousKey configured — nothing to rotate from')
|
|
@@ -71,7 +71,7 @@ function registerTests(name: string, getDb: () => Db) {
|
|
|
71
71
|
auditReads: true,
|
|
72
72
|
})
|
|
73
73
|
await service.init()
|
|
74
|
-
await service.
|
|
74
|
+
await service.setSecret('audit-test', 'value')
|
|
75
75
|
await service.getSecret('audit-test')
|
|
76
76
|
await service.deleteSecret('audit-test')
|
|
77
77
|
|
|
@@ -94,7 +94,7 @@ function registerTests(name: string, getDb: () => Db) {
|
|
|
94
94
|
auditReads: false,
|
|
95
95
|
})
|
|
96
96
|
await service.init()
|
|
97
|
-
await service.
|
|
97
|
+
await service.setSecret('no-read-audit', 'value')
|
|
98
98
|
await service.getSecret('no-read-audit')
|
|
99
99
|
|
|
100
100
|
const logs = await getDb()
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type WorkflowRun,
|
|
6
6
|
type WorkflowRunWire,
|
|
7
7
|
type StepState,
|
|
8
|
+
type StepStatus,
|
|
8
9
|
type WorkflowStatus,
|
|
9
10
|
type WorkflowVersionStatus,
|
|
10
11
|
} from '@pikku/core/workflow'
|
|
@@ -41,6 +42,7 @@ interface WorkflowStepDoc {
|
|
|
41
42
|
branchTaken: string | null
|
|
42
43
|
retries: number | null
|
|
43
44
|
retryDelay: string | null
|
|
45
|
+
fromStepName?: string | null
|
|
44
46
|
createdAt: Date
|
|
45
47
|
updatedAt: Date
|
|
46
48
|
}
|
|
@@ -110,7 +112,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
110
112
|
this.initialized = true
|
|
111
113
|
}
|
|
112
114
|
|
|
113
|
-
async
|
|
115
|
+
protected async createRunImpl(
|
|
114
116
|
workflowName: string,
|
|
115
117
|
input: any,
|
|
116
118
|
inline: boolean,
|
|
@@ -148,7 +150,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
148
150
|
return this.runService.getRun(id)
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
async
|
|
153
|
+
protected async updateRunStatusImpl(
|
|
152
154
|
id: string,
|
|
153
155
|
status: WorkflowStatus,
|
|
154
156
|
output?: any,
|
|
@@ -167,12 +169,13 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
167
169
|
)
|
|
168
170
|
}
|
|
169
171
|
|
|
170
|
-
async
|
|
172
|
+
protected async insertStepStateImpl(
|
|
171
173
|
runId: string,
|
|
172
174
|
stepName: string,
|
|
173
175
|
rpcName: string | null,
|
|
174
176
|
data: any,
|
|
175
|
-
stepOptions?: { retries?: number; retryDelay?: string | number }
|
|
177
|
+
stepOptions?: { retries?: number; retryDelay?: string | number },
|
|
178
|
+
fromStepName?: string
|
|
176
179
|
): Promise<StepState> {
|
|
177
180
|
const stepId = crypto.randomUUID()
|
|
178
181
|
const now = new Date()
|
|
@@ -189,6 +192,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
189
192
|
branchTaken: null,
|
|
190
193
|
retries: stepOptions?.retries ?? null,
|
|
191
194
|
retryDelay: stepOptions?.retryDelay?.toString() ?? null,
|
|
195
|
+
fromStepName: fromStepName ?? null,
|
|
192
196
|
createdAt: now,
|
|
193
197
|
updatedAt: now,
|
|
194
198
|
})
|
|
@@ -203,6 +207,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
203
207
|
attemptCount: 1,
|
|
204
208
|
retries: stepOptions?.retries,
|
|
205
209
|
retryDelay: stepOptions?.retryDelay?.toString(),
|
|
210
|
+
fromStepName,
|
|
206
211
|
createdAt: now,
|
|
207
212
|
updatedAt: now,
|
|
208
213
|
}
|
|
@@ -232,6 +237,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
232
237
|
attemptCount,
|
|
233
238
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
234
239
|
retryDelay: row.retryDelay ?? undefined,
|
|
240
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
235
241
|
createdAt: new Date(row.createdAt),
|
|
236
242
|
updatedAt: new Date(row.updatedAt),
|
|
237
243
|
}
|
|
@@ -243,7 +249,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
243
249
|
return this.runService.getRunHistory(runId)
|
|
244
250
|
}
|
|
245
251
|
|
|
246
|
-
async
|
|
252
|
+
protected async setStepRunningImpl(stepId: string): Promise<void> {
|
|
247
253
|
await this.steps.updateOne(
|
|
248
254
|
{ _id: stepId },
|
|
249
255
|
{ $set: { status: 'running', updatedAt: new Date() } }
|
|
@@ -263,7 +269,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
263
269
|
}
|
|
264
270
|
}
|
|
265
271
|
|
|
266
|
-
async
|
|
272
|
+
protected async setStepScheduledImpl(stepId: string): Promise<void> {
|
|
267
273
|
await this.steps.updateOne(
|
|
268
274
|
{ _id: stepId },
|
|
269
275
|
{ $set: { status: 'scheduled', updatedAt: new Date() } }
|
|
@@ -313,7 +319,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
313
319
|
}
|
|
314
320
|
}
|
|
315
321
|
|
|
316
|
-
async
|
|
322
|
+
protected async setStepChildRunIdImpl(
|
|
323
|
+
stepId: string,
|
|
324
|
+
childRunId: string
|
|
325
|
+
): Promise<void> {
|
|
317
326
|
await this.steps.updateOne(
|
|
318
327
|
{ _id: stepId },
|
|
319
328
|
{
|
|
@@ -325,7 +334,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
325
334
|
)
|
|
326
335
|
}
|
|
327
336
|
|
|
328
|
-
async
|
|
337
|
+
protected async setStepResultImpl(
|
|
338
|
+
stepId: string,
|
|
339
|
+
result: any
|
|
340
|
+
): Promise<void> {
|
|
329
341
|
await this.steps.updateOne(
|
|
330
342
|
{ _id: stepId },
|
|
331
343
|
{
|
|
@@ -352,7 +364,10 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
352
364
|
}
|
|
353
365
|
}
|
|
354
366
|
|
|
355
|
-
async
|
|
367
|
+
protected async setStepErrorImpl(
|
|
368
|
+
stepId: string,
|
|
369
|
+
error: Error
|
|
370
|
+
): Promise<void> {
|
|
356
371
|
const serializedError: SerializedError = {
|
|
357
372
|
message: error.message,
|
|
358
373
|
stack: error.stack,
|
|
@@ -385,7 +400,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
385
400
|
}
|
|
386
401
|
}
|
|
387
402
|
|
|
388
|
-
async
|
|
403
|
+
protected async createRetryAttemptImpl(
|
|
389
404
|
stepId: string,
|
|
390
405
|
status: 'pending' | 'running'
|
|
391
406
|
): Promise<StepState> {
|
|
@@ -418,6 +433,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
418
433
|
attemptCount,
|
|
419
434
|
retries: row.retries != null ? Number(row.retries) : undefined,
|
|
420
435
|
retryDelay: row.retryDelay ?? undefined,
|
|
436
|
+
fromStepName: row.fromStepName ?? undefined,
|
|
421
437
|
createdAt: new Date(row.createdAt),
|
|
422
438
|
updatedAt: new Date(row.updatedAt),
|
|
423
439
|
}
|
|
@@ -491,6 +507,20 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
491
507
|
return nodeIds.filter((id) => !existingStepNames.has(id))
|
|
492
508
|
}
|
|
493
509
|
|
|
510
|
+
async getStepInstances(runId: string): Promise<
|
|
511
|
+
Array<{ stepName: string; status: StepStatus; fromStepName?: string }>
|
|
512
|
+
> {
|
|
513
|
+
const rows = await this.steps
|
|
514
|
+
.find({ workflowRunId: runId })
|
|
515
|
+
.project({ stepName: 1, status: 1, fromStepName: 1 })
|
|
516
|
+
.toArray()
|
|
517
|
+
return rows.map((r: any) => ({
|
|
518
|
+
stepName: r.stepName,
|
|
519
|
+
status: r.status as StepStatus,
|
|
520
|
+
fromStepName: r.fromStepName ?? undefined,
|
|
521
|
+
}))
|
|
522
|
+
}
|
|
523
|
+
|
|
494
524
|
async getNodeResults(
|
|
495
525
|
runId: string,
|
|
496
526
|
nodeIds: string[]
|
|
@@ -512,14 +542,17 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
512
542
|
return results
|
|
513
543
|
}
|
|
514
544
|
|
|
515
|
-
async
|
|
545
|
+
protected async setBranchTakenImpl(
|
|
546
|
+
stepId: string,
|
|
547
|
+
branchKey: string
|
|
548
|
+
): Promise<void> {
|
|
516
549
|
await this.steps.updateOne(
|
|
517
550
|
{ _id: stepId },
|
|
518
551
|
{ $set: { branchTaken: branchKey, updatedAt: new Date() } }
|
|
519
552
|
)
|
|
520
553
|
}
|
|
521
554
|
|
|
522
|
-
async
|
|
555
|
+
protected async updateRunStateImpl(
|
|
523
556
|
runId: string,
|
|
524
557
|
name: string,
|
|
525
558
|
value: unknown
|
|
@@ -547,7 +580,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
547
580
|
return row.state ?? {}
|
|
548
581
|
}
|
|
549
582
|
|
|
550
|
-
async
|
|
583
|
+
protected async upsertWorkflowVersionImpl(
|
|
551
584
|
name: string,
|
|
552
585
|
graphHash: string,
|
|
553
586
|
graph: any,
|
|
@@ -570,7 +603,7 @@ export class MongoDBWorkflowService extends PikkuWorkflowService {
|
|
|
570
603
|
)
|
|
571
604
|
}
|
|
572
605
|
|
|
573
|
-
async
|
|
606
|
+
protected async updateWorkflowVersionStatusImpl(
|
|
574
607
|
name: string,
|
|
575
608
|
graphHash: string,
|
|
576
609
|
status: WorkflowVersionStatus
|