@hasna/loops 0.3.52 → 0.3.54
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/README.md +45 -0
- package/dist/cli/index.js +189 -51
- package/dist/daemon/index.js +118 -12
- package/dist/index.js +141 -20
- package/dist/lib/store.d.ts +1 -0
- package/dist/lib/store.js +114 -8
- package/dist/lib/workflow-spec.d.ts +8 -2
- package/dist/sdk/index.js +114 -8
- package/dist/types.d.ts +19 -4
- package/docs/USAGE.md +50 -0
- package/package.json +1 -1
package/dist/lib/store.js
CHANGED
|
@@ -312,6 +312,8 @@ function updateReadyFlags(nodes, planStatus) {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
// src/lib/workflow-spec.ts
|
|
315
|
+
import { readFileSync } from "fs";
|
|
316
|
+
import { isAbsolute, resolve } from "path";
|
|
315
317
|
function assertObject(value, label) {
|
|
316
318
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
317
319
|
throw new Error(`${label} must be an object`);
|
|
@@ -357,6 +359,38 @@ function optionalAccountRef(value, label) {
|
|
|
357
359
|
tool: typeof value.tool === "string" ? value.tool.trim() : undefined
|
|
358
360
|
};
|
|
359
361
|
}
|
|
362
|
+
function promptFilePath(rawPath, opts) {
|
|
363
|
+
return isAbsolute(rawPath) ? resolve(rawPath) : resolve(opts.baseDir ?? process.cwd(), rawPath);
|
|
364
|
+
}
|
|
365
|
+
function readPromptFile(rawPath, label, opts) {
|
|
366
|
+
assertString(rawPath, `${label}.promptFile`);
|
|
367
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
368
|
+
let prompt;
|
|
369
|
+
try {
|
|
370
|
+
prompt = readFileSync(path, "utf8");
|
|
371
|
+
} catch (error) {
|
|
372
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
373
|
+
throw new Error(`${label}.promptFile could not be read${code ? `: ${code}` : ""}`);
|
|
374
|
+
}
|
|
375
|
+
if (prompt.trim() === "")
|
|
376
|
+
throw new Error(`${label}.promptFile must contain a non-empty prompt`);
|
|
377
|
+
return { prompt, promptSource: { type: "file", path } };
|
|
378
|
+
}
|
|
379
|
+
function matchingPromptSource(value, prompt, opts) {
|
|
380
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
381
|
+
return;
|
|
382
|
+
if (value.type !== "file")
|
|
383
|
+
return;
|
|
384
|
+
const rawPath = value.path;
|
|
385
|
+
if (typeof rawPath !== "string" || rawPath.trim() === "")
|
|
386
|
+
return;
|
|
387
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
388
|
+
try {
|
|
389
|
+
return readFileSync(path, "utf8") === prompt ? { type: "file", path } : undefined;
|
|
390
|
+
} catch {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
360
394
|
function normalizeGoalSpec(value, label = "goal") {
|
|
361
395
|
if (value === undefined)
|
|
362
396
|
return;
|
|
@@ -379,7 +413,7 @@ function normalizeGoalSpec(value, label = "goal") {
|
|
|
379
413
|
autoExecute
|
|
380
414
|
};
|
|
381
415
|
}
|
|
382
|
-
function validateTarget(value, label) {
|
|
416
|
+
function validateTarget(value, label, opts) {
|
|
383
417
|
assertObject(value, label);
|
|
384
418
|
if (value.type === "command") {
|
|
385
419
|
assertString(value.command, `${label}.command`);
|
|
@@ -392,7 +426,13 @@ function validateTarget(value, label) {
|
|
|
392
426
|
}
|
|
393
427
|
if (value.type === "agent") {
|
|
394
428
|
assertString(value.provider, `${label}.provider`);
|
|
395
|
-
|
|
429
|
+
const hasPrompt = typeof value.prompt === "string" && value.prompt.trim() !== "";
|
|
430
|
+
const hasPromptFile = typeof value.promptFile === "string" && value.promptFile.trim() !== "";
|
|
431
|
+
if (hasPrompt && hasPromptFile)
|
|
432
|
+
throw new Error(`${label} must use either prompt or promptFile, not both`);
|
|
433
|
+
if (!hasPrompt && !hasPromptFile)
|
|
434
|
+
throw new Error(`${label}.prompt must be a non-empty string or ${label}.promptFile must be set`);
|
|
435
|
+
const promptFields = hasPrompt ? { prompt: value.prompt, promptSource: matchingPromptSource(value.promptSource, value.prompt, opts) } : readPromptFile(value.promptFile, label, opts);
|
|
396
436
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
397
437
|
if (!providers.includes(value.provider))
|
|
398
438
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
@@ -493,11 +533,14 @@ function validateTarget(value, label) {
|
|
|
493
533
|
if (value.routing.eventSource !== undefined)
|
|
494
534
|
assertString(value.routing.eventSource, `${label}.routing.eventSource`);
|
|
495
535
|
}
|
|
496
|
-
|
|
536
|
+
const target = { ...value };
|
|
537
|
+
delete target.promptFile;
|
|
538
|
+
delete target.promptSource;
|
|
539
|
+
return { ...target, ...promptFields };
|
|
497
540
|
}
|
|
498
541
|
throw new Error(`${label}.type must be command or agent`);
|
|
499
542
|
}
|
|
500
|
-
function normalizeCreateWorkflowInput(input) {
|
|
543
|
+
function normalizeCreateWorkflowInput(input, opts = {}) {
|
|
501
544
|
assertString(input.name, "workflow.name");
|
|
502
545
|
const goal = normalizeGoalSpec(input.goal, "goal");
|
|
503
546
|
if (!Array.isArray(input.steps) || input.steps.length === 0)
|
|
@@ -513,7 +556,7 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
513
556
|
...step,
|
|
514
557
|
id: step.id,
|
|
515
558
|
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
516
|
-
target: validateTarget(step.target, `workflow.steps[${index}].target
|
|
559
|
+
target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
|
|
517
560
|
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
518
561
|
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
519
562
|
timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
@@ -556,7 +599,7 @@ function workflowExecutionOrder(workflow) {
|
|
|
556
599
|
visit(step);
|
|
557
600
|
return order;
|
|
558
601
|
}
|
|
559
|
-
function workflowBodyFromJson(value, fallbackName) {
|
|
602
|
+
function workflowBodyFromJson(value, fallbackName, opts = {}) {
|
|
560
603
|
assertObject(value, "workflow file");
|
|
561
604
|
const rawName = fallbackName ?? value.name;
|
|
562
605
|
assertString(rawName, "workflow.name");
|
|
@@ -568,7 +611,7 @@ function workflowBodyFromJson(value, fallbackName) {
|
|
|
568
611
|
goal: normalizeGoalSpec(value.goal, "goal"),
|
|
569
612
|
version: typeof value.version === "number" ? value.version : undefined,
|
|
570
613
|
steps: value.steps
|
|
571
|
-
});
|
|
614
|
+
}, opts);
|
|
572
615
|
}
|
|
573
616
|
|
|
574
617
|
// src/lib/run-artifacts.ts
|
|
@@ -1205,13 +1248,17 @@ class Store {
|
|
|
1205
1248
|
}
|
|
1206
1249
|
createLoop(input, from = new Date) {
|
|
1207
1250
|
const now = nowIso();
|
|
1251
|
+
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1252
|
+
name: "loop-target-validation",
|
|
1253
|
+
steps: [{ id: "target", target: input.target }]
|
|
1254
|
+
}).steps[0].target;
|
|
1208
1255
|
const loop = {
|
|
1209
1256
|
id: genId(),
|
|
1210
1257
|
name: input.name,
|
|
1211
1258
|
description: input.description,
|
|
1212
1259
|
status: "active",
|
|
1213
1260
|
schedule: input.schedule,
|
|
1214
|
-
target
|
|
1261
|
+
target,
|
|
1215
1262
|
goal: input.goal,
|
|
1216
1263
|
machine: input.machine,
|
|
1217
1264
|
nextRunAt: initialNextRun(input.schedule, from),
|
|
@@ -1565,6 +1612,65 @@ class Store {
|
|
|
1565
1612
|
throw new Error(`workflow invocation not found after create: ${id}`);
|
|
1566
1613
|
return rowToWorkflowInvocation(row);
|
|
1567
1614
|
}
|
|
1615
|
+
refreshWorkflowInvocationForWorkItem(workItemId, input) {
|
|
1616
|
+
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
1617
|
+
if (!sourceDedupeKey)
|
|
1618
|
+
throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
|
|
1619
|
+
const now = nowIso();
|
|
1620
|
+
const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
|
|
1621
|
+
const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
|
|
1622
|
+
const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
|
|
1623
|
+
const result = this.db.query(`UPDATE workflow_invocations
|
|
1624
|
+
SET workflow_id=COALESCE($workflowId, workflow_id),
|
|
1625
|
+
template_id=COALESCE($templateId, template_id),
|
|
1626
|
+
source_id=COALESCE($sourceId, source_id),
|
|
1627
|
+
source_json=$sourceJson,
|
|
1628
|
+
subject_kind=$subjectKind,
|
|
1629
|
+
subject_id=COALESCE($subjectId, subject_id),
|
|
1630
|
+
subject_path=COALESCE($subjectPath, subject_path),
|
|
1631
|
+
subject_url=COALESCE($subjectUrl, subject_url),
|
|
1632
|
+
subject_json=$subjectJson,
|
|
1633
|
+
intent=$intent,
|
|
1634
|
+
scope_json=COALESCE($scopeJson, scope_json),
|
|
1635
|
+
output_policy_json=COALESCE($outputPolicyJson, output_policy_json),
|
|
1636
|
+
updated_at=$updated
|
|
1637
|
+
WHERE source_kind=$sourceKind
|
|
1638
|
+
AND source_dedupe_key=$sourceDedupeKey
|
|
1639
|
+
AND EXISTS (
|
|
1640
|
+
SELECT 1
|
|
1641
|
+
FROM workflow_work_items
|
|
1642
|
+
WHERE id=$workItemId
|
|
1643
|
+
AND invocation_id=workflow_invocations.id
|
|
1644
|
+
AND status IN (${placeholders})
|
|
1645
|
+
)`).run({
|
|
1646
|
+
$workItemId: workItemId,
|
|
1647
|
+
$sourceKind: input.sourceRef.kind,
|
|
1648
|
+
$sourceDedupeKey: sourceDedupeKey,
|
|
1649
|
+
$workflowId: input.workflowId ?? null,
|
|
1650
|
+
$templateId: input.templateId ?? null,
|
|
1651
|
+
$sourceId: input.sourceRef.id ?? null,
|
|
1652
|
+
$sourceJson: JSON.stringify(input.sourceRef),
|
|
1653
|
+
$subjectKind: input.subjectRef.kind,
|
|
1654
|
+
$subjectId: input.subjectRef.id ?? null,
|
|
1655
|
+
$subjectPath: input.subjectRef.path ?? null,
|
|
1656
|
+
$subjectUrl: input.subjectRef.url ?? null,
|
|
1657
|
+
$subjectJson: JSON.stringify(input.subjectRef),
|
|
1658
|
+
$intent: input.intent,
|
|
1659
|
+
$scopeJson: input.scope ? JSON.stringify(input.scope) : null,
|
|
1660
|
+
$outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
|
|
1661
|
+
$updated: now,
|
|
1662
|
+
...statusBindings
|
|
1663
|
+
});
|
|
1664
|
+
if (result.changes !== 1)
|
|
1665
|
+
throw new Error(`workflow work item is not refreshable: ${workItemId}`);
|
|
1666
|
+
const updated = this.db.query(`SELECT workflow_invocations.*
|
|
1667
|
+
FROM workflow_invocations
|
|
1668
|
+
JOIN workflow_work_items ON workflow_work_items.invocation_id = workflow_invocations.id
|
|
1669
|
+
WHERE workflow_work_items.id = ?`).get(workItemId);
|
|
1670
|
+
if (!updated)
|
|
1671
|
+
throw new Error(`workflow invocation not found after refresh for work item: ${workItemId}`);
|
|
1672
|
+
return rowToWorkflowInvocation(updated);
|
|
1673
|
+
}
|
|
1568
1674
|
getWorkflowInvocation(id) {
|
|
1569
1675
|
const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
|
|
1570
1676
|
return row ? rowToWorkflowInvocation(row) : undefined;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { CreateWorkflowInput, GoalSpec, WorkflowSpec, WorkflowStep } from "../types.js";
|
|
2
2
|
export type WorkflowSpecBody = Pick<WorkflowSpec, "name" | "description" | "version" | "steps">;
|
|
3
|
+
export type NormalizedCreateWorkflowInput = Omit<CreateWorkflowInput, "steps"> & {
|
|
4
|
+
steps: WorkflowStep[];
|
|
5
|
+
};
|
|
6
|
+
export interface WorkflowNormalizeOptions {
|
|
7
|
+
baseDir?: string;
|
|
8
|
+
}
|
|
3
9
|
export declare function normalizeGoalSpec(value: unknown, label?: string): GoalSpec | undefined;
|
|
4
|
-
export declare function normalizeCreateWorkflowInput(input: CreateWorkflowInput):
|
|
10
|
+
export declare function normalizeCreateWorkflowInput(input: CreateWorkflowInput, opts?: WorkflowNormalizeOptions): NormalizedCreateWorkflowInput;
|
|
5
11
|
export declare function workflowExecutionOrder(workflow: Pick<WorkflowSpec, "steps">): WorkflowStep[];
|
|
6
|
-
export declare function workflowBodyFromJson(value: unknown, fallbackName?: string):
|
|
12
|
+
export declare function workflowBodyFromJson(value: unknown, fallbackName?: string, opts?: WorkflowNormalizeOptions): NormalizedCreateWorkflowInput;
|
package/dist/sdk/index.js
CHANGED
|
@@ -312,6 +312,8 @@ function updateReadyFlags(nodes, planStatus) {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
// src/lib/workflow-spec.ts
|
|
315
|
+
import { readFileSync } from "fs";
|
|
316
|
+
import { isAbsolute, resolve } from "path";
|
|
315
317
|
function assertObject(value, label) {
|
|
316
318
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
317
319
|
throw new Error(`${label} must be an object`);
|
|
@@ -357,6 +359,38 @@ function optionalAccountRef(value, label) {
|
|
|
357
359
|
tool: typeof value.tool === "string" ? value.tool.trim() : undefined
|
|
358
360
|
};
|
|
359
361
|
}
|
|
362
|
+
function promptFilePath(rawPath, opts) {
|
|
363
|
+
return isAbsolute(rawPath) ? resolve(rawPath) : resolve(opts.baseDir ?? process.cwd(), rawPath);
|
|
364
|
+
}
|
|
365
|
+
function readPromptFile(rawPath, label, opts) {
|
|
366
|
+
assertString(rawPath, `${label}.promptFile`);
|
|
367
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
368
|
+
let prompt;
|
|
369
|
+
try {
|
|
370
|
+
prompt = readFileSync(path, "utf8");
|
|
371
|
+
} catch (error) {
|
|
372
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : undefined;
|
|
373
|
+
throw new Error(`${label}.promptFile could not be read${code ? `: ${code}` : ""}`);
|
|
374
|
+
}
|
|
375
|
+
if (prompt.trim() === "")
|
|
376
|
+
throw new Error(`${label}.promptFile must contain a non-empty prompt`);
|
|
377
|
+
return { prompt, promptSource: { type: "file", path } };
|
|
378
|
+
}
|
|
379
|
+
function matchingPromptSource(value, prompt, opts) {
|
|
380
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
381
|
+
return;
|
|
382
|
+
if (value.type !== "file")
|
|
383
|
+
return;
|
|
384
|
+
const rawPath = value.path;
|
|
385
|
+
if (typeof rawPath !== "string" || rawPath.trim() === "")
|
|
386
|
+
return;
|
|
387
|
+
const path = promptFilePath(rawPath.trim(), opts);
|
|
388
|
+
try {
|
|
389
|
+
return readFileSync(path, "utf8") === prompt ? { type: "file", path } : undefined;
|
|
390
|
+
} catch {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
360
394
|
function normalizeGoalSpec(value, label = "goal") {
|
|
361
395
|
if (value === undefined)
|
|
362
396
|
return;
|
|
@@ -379,7 +413,7 @@ function normalizeGoalSpec(value, label = "goal") {
|
|
|
379
413
|
autoExecute
|
|
380
414
|
};
|
|
381
415
|
}
|
|
382
|
-
function validateTarget(value, label) {
|
|
416
|
+
function validateTarget(value, label, opts) {
|
|
383
417
|
assertObject(value, label);
|
|
384
418
|
if (value.type === "command") {
|
|
385
419
|
assertString(value.command, `${label}.command`);
|
|
@@ -392,7 +426,13 @@ function validateTarget(value, label) {
|
|
|
392
426
|
}
|
|
393
427
|
if (value.type === "agent") {
|
|
394
428
|
assertString(value.provider, `${label}.provider`);
|
|
395
|
-
|
|
429
|
+
const hasPrompt = typeof value.prompt === "string" && value.prompt.trim() !== "";
|
|
430
|
+
const hasPromptFile = typeof value.promptFile === "string" && value.promptFile.trim() !== "";
|
|
431
|
+
if (hasPrompt && hasPromptFile)
|
|
432
|
+
throw new Error(`${label} must use either prompt or promptFile, not both`);
|
|
433
|
+
if (!hasPrompt && !hasPromptFile)
|
|
434
|
+
throw new Error(`${label}.prompt must be a non-empty string or ${label}.promptFile must be set`);
|
|
435
|
+
const promptFields = hasPrompt ? { prompt: value.prompt, promptSource: matchingPromptSource(value.promptSource, value.prompt, opts) } : readPromptFile(value.promptFile, label, opts);
|
|
396
436
|
const providers = ["claude", "cursor", "codewith", "aicopilot", "opencode", "codex"];
|
|
397
437
|
if (!providers.includes(value.provider))
|
|
398
438
|
throw new Error(`${label}.provider must be one of ${providers.join(", ")}`);
|
|
@@ -493,11 +533,14 @@ function validateTarget(value, label) {
|
|
|
493
533
|
if (value.routing.eventSource !== undefined)
|
|
494
534
|
assertString(value.routing.eventSource, `${label}.routing.eventSource`);
|
|
495
535
|
}
|
|
496
|
-
|
|
536
|
+
const target = { ...value };
|
|
537
|
+
delete target.promptFile;
|
|
538
|
+
delete target.promptSource;
|
|
539
|
+
return { ...target, ...promptFields };
|
|
497
540
|
}
|
|
498
541
|
throw new Error(`${label}.type must be command or agent`);
|
|
499
542
|
}
|
|
500
|
-
function normalizeCreateWorkflowInput(input) {
|
|
543
|
+
function normalizeCreateWorkflowInput(input, opts = {}) {
|
|
501
544
|
assertString(input.name, "workflow.name");
|
|
502
545
|
const goal = normalizeGoalSpec(input.goal, "goal");
|
|
503
546
|
if (!Array.isArray(input.steps) || input.steps.length === 0)
|
|
@@ -513,7 +556,7 @@ function normalizeCreateWorkflowInput(input) {
|
|
|
513
556
|
...step,
|
|
514
557
|
id: step.id,
|
|
515
558
|
goal: normalizeGoalSpec(step.goal, `workflow.steps[${index}].goal`),
|
|
516
|
-
target: validateTarget(step.target, `workflow.steps[${index}].target
|
|
559
|
+
target: validateTarget(step.target, `workflow.steps[${index}].target`, opts),
|
|
517
560
|
dependsOn: optionalStringArray(step.dependsOn, `workflow.steps[${index}].dependsOn`) ?? [],
|
|
518
561
|
continueOnFailure: optionalBoolean(step.continueOnFailure, `workflow.steps[${index}].continueOnFailure`) ?? false,
|
|
519
562
|
timeoutMs: optionalPositiveInteger(step.timeoutMs, `workflow.steps[${index}].timeoutMs`),
|
|
@@ -556,7 +599,7 @@ function workflowExecutionOrder(workflow) {
|
|
|
556
599
|
visit(step);
|
|
557
600
|
return order;
|
|
558
601
|
}
|
|
559
|
-
function workflowBodyFromJson(value, fallbackName) {
|
|
602
|
+
function workflowBodyFromJson(value, fallbackName, opts = {}) {
|
|
560
603
|
assertObject(value, "workflow file");
|
|
561
604
|
const rawName = fallbackName ?? value.name;
|
|
562
605
|
assertString(rawName, "workflow.name");
|
|
@@ -568,7 +611,7 @@ function workflowBodyFromJson(value, fallbackName) {
|
|
|
568
611
|
goal: normalizeGoalSpec(value.goal, "goal"),
|
|
569
612
|
version: typeof value.version === "number" ? value.version : undefined,
|
|
570
613
|
steps: value.steps
|
|
571
|
-
});
|
|
614
|
+
}, opts);
|
|
572
615
|
}
|
|
573
616
|
|
|
574
617
|
// src/lib/run-artifacts.ts
|
|
@@ -1205,13 +1248,17 @@ class Store {
|
|
|
1205
1248
|
}
|
|
1206
1249
|
createLoop(input, from = new Date) {
|
|
1207
1250
|
const now = nowIso();
|
|
1251
|
+
const target = input.target.type === "workflow" ? input.target : normalizeCreateWorkflowInput({
|
|
1252
|
+
name: "loop-target-validation",
|
|
1253
|
+
steps: [{ id: "target", target: input.target }]
|
|
1254
|
+
}).steps[0].target;
|
|
1208
1255
|
const loop = {
|
|
1209
1256
|
id: genId(),
|
|
1210
1257
|
name: input.name,
|
|
1211
1258
|
description: input.description,
|
|
1212
1259
|
status: "active",
|
|
1213
1260
|
schedule: input.schedule,
|
|
1214
|
-
target
|
|
1261
|
+
target,
|
|
1215
1262
|
goal: input.goal,
|
|
1216
1263
|
machine: input.machine,
|
|
1217
1264
|
nextRunAt: initialNextRun(input.schedule, from),
|
|
@@ -1565,6 +1612,65 @@ class Store {
|
|
|
1565
1612
|
throw new Error(`workflow invocation not found after create: ${id}`);
|
|
1566
1613
|
return rowToWorkflowInvocation(row);
|
|
1567
1614
|
}
|
|
1615
|
+
refreshWorkflowInvocationForWorkItem(workItemId, input) {
|
|
1616
|
+
const sourceDedupeKey = input.sourceRef.dedupeKey ?? undefined;
|
|
1617
|
+
if (!sourceDedupeKey)
|
|
1618
|
+
throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
|
|
1619
|
+
const now = nowIso();
|
|
1620
|
+
const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
|
|
1621
|
+
const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
|
|
1622
|
+
const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
|
|
1623
|
+
const result = this.db.query(`UPDATE workflow_invocations
|
|
1624
|
+
SET workflow_id=COALESCE($workflowId, workflow_id),
|
|
1625
|
+
template_id=COALESCE($templateId, template_id),
|
|
1626
|
+
source_id=COALESCE($sourceId, source_id),
|
|
1627
|
+
source_json=$sourceJson,
|
|
1628
|
+
subject_kind=$subjectKind,
|
|
1629
|
+
subject_id=COALESCE($subjectId, subject_id),
|
|
1630
|
+
subject_path=COALESCE($subjectPath, subject_path),
|
|
1631
|
+
subject_url=COALESCE($subjectUrl, subject_url),
|
|
1632
|
+
subject_json=$subjectJson,
|
|
1633
|
+
intent=$intent,
|
|
1634
|
+
scope_json=COALESCE($scopeJson, scope_json),
|
|
1635
|
+
output_policy_json=COALESCE($outputPolicyJson, output_policy_json),
|
|
1636
|
+
updated_at=$updated
|
|
1637
|
+
WHERE source_kind=$sourceKind
|
|
1638
|
+
AND source_dedupe_key=$sourceDedupeKey
|
|
1639
|
+
AND EXISTS (
|
|
1640
|
+
SELECT 1
|
|
1641
|
+
FROM workflow_work_items
|
|
1642
|
+
WHERE id=$workItemId
|
|
1643
|
+
AND invocation_id=workflow_invocations.id
|
|
1644
|
+
AND status IN (${placeholders})
|
|
1645
|
+
)`).run({
|
|
1646
|
+
$workItemId: workItemId,
|
|
1647
|
+
$sourceKind: input.sourceRef.kind,
|
|
1648
|
+
$sourceDedupeKey: sourceDedupeKey,
|
|
1649
|
+
$workflowId: input.workflowId ?? null,
|
|
1650
|
+
$templateId: input.templateId ?? null,
|
|
1651
|
+
$sourceId: input.sourceRef.id ?? null,
|
|
1652
|
+
$sourceJson: JSON.stringify(input.sourceRef),
|
|
1653
|
+
$subjectKind: input.subjectRef.kind,
|
|
1654
|
+
$subjectId: input.subjectRef.id ?? null,
|
|
1655
|
+
$subjectPath: input.subjectRef.path ?? null,
|
|
1656
|
+
$subjectUrl: input.subjectRef.url ?? null,
|
|
1657
|
+
$subjectJson: JSON.stringify(input.subjectRef),
|
|
1658
|
+
$intent: input.intent,
|
|
1659
|
+
$scopeJson: input.scope ? JSON.stringify(input.scope) : null,
|
|
1660
|
+
$outputPolicyJson: input.outputPolicy ? JSON.stringify(input.outputPolicy) : null,
|
|
1661
|
+
$updated: now,
|
|
1662
|
+
...statusBindings
|
|
1663
|
+
});
|
|
1664
|
+
if (result.changes !== 1)
|
|
1665
|
+
throw new Error(`workflow work item is not refreshable: ${workItemId}`);
|
|
1666
|
+
const updated = this.db.query(`SELECT workflow_invocations.*
|
|
1667
|
+
FROM workflow_invocations
|
|
1668
|
+
JOIN workflow_work_items ON workflow_work_items.invocation_id = workflow_invocations.id
|
|
1669
|
+
WHERE workflow_work_items.id = ?`).get(workItemId);
|
|
1670
|
+
if (!updated)
|
|
1671
|
+
throw new Error(`workflow invocation not found after refresh for work item: ${workItemId}`);
|
|
1672
|
+
return rowToWorkflowInvocation(updated);
|
|
1673
|
+
}
|
|
1568
1674
|
getWorkflowInvocation(id) {
|
|
1569
1675
|
const row = this.db.query("SELECT * FROM workflow_invocations WHERE id = ?").get(id);
|
|
1570
1676
|
return row ? rowToWorkflowInvocation(row) : undefined;
|
package/dist/types.d.ts
CHANGED
|
@@ -104,10 +104,13 @@ export interface AgentRoutingSpec {
|
|
|
104
104
|
eventType?: string;
|
|
105
105
|
eventSource?: string;
|
|
106
106
|
}
|
|
107
|
-
export interface
|
|
107
|
+
export interface AgentPromptSource {
|
|
108
|
+
type: "file";
|
|
109
|
+
path: string;
|
|
110
|
+
}
|
|
111
|
+
export interface AgentTargetBase {
|
|
108
112
|
type: "agent";
|
|
109
113
|
provider: AgentProvider;
|
|
110
|
-
prompt: string;
|
|
111
114
|
cwd?: string;
|
|
112
115
|
model?: string;
|
|
113
116
|
variant?: string;
|
|
@@ -126,6 +129,13 @@ export interface AgentTarget {
|
|
|
126
129
|
account?: AccountRef;
|
|
127
130
|
preflight?: RuntimePreflightPolicy;
|
|
128
131
|
}
|
|
132
|
+
export interface AgentTarget extends AgentTargetBase {
|
|
133
|
+
prompt: string;
|
|
134
|
+
promptSource?: AgentPromptSource;
|
|
135
|
+
}
|
|
136
|
+
export interface PromptFileAgentTarget extends AgentTargetBase {
|
|
137
|
+
promptFile: string;
|
|
138
|
+
}
|
|
129
139
|
export interface WorkflowTarget {
|
|
130
140
|
type: "workflow";
|
|
131
141
|
workflowId: string;
|
|
@@ -134,7 +144,9 @@ export interface WorkflowTarget {
|
|
|
134
144
|
preflight?: RuntimePreflightPolicy;
|
|
135
145
|
}
|
|
136
146
|
export type ExecutableTarget = CommandTarget | AgentTarget;
|
|
147
|
+
export type ExecutableTargetInput = CommandTarget | AgentTarget | PromptFileAgentTarget;
|
|
137
148
|
export type LoopTarget = ExecutableTarget | WorkflowTarget;
|
|
149
|
+
export type LoopTargetInput = ExecutableTargetInput | WorkflowTarget;
|
|
138
150
|
export type WorkflowStatus = "active" | "archived";
|
|
139
151
|
export type WorkflowRunStatus = "running" | "succeeded" | "failed" | "timed_out" | "cancelled";
|
|
140
152
|
export type WorkflowStepRunStatus = "pending" | "running" | "succeeded" | "failed" | "timed_out" | "skipped" | "cancelled";
|
|
@@ -233,6 +245,9 @@ export interface WorkflowStep {
|
|
|
233
245
|
timeoutMs?: number;
|
|
234
246
|
account?: AccountRef;
|
|
235
247
|
}
|
|
248
|
+
export interface WorkflowStepInput extends Omit<WorkflowStep, "target"> {
|
|
249
|
+
target: ExecutableTargetInput;
|
|
250
|
+
}
|
|
236
251
|
export interface WorkflowSpec {
|
|
237
252
|
id: string;
|
|
238
253
|
name: string;
|
|
@@ -248,7 +263,7 @@ export interface CreateWorkflowInput {
|
|
|
248
263
|
name: string;
|
|
249
264
|
description?: string;
|
|
250
265
|
goal?: GoalSpec;
|
|
251
|
-
steps:
|
|
266
|
+
steps: WorkflowStepInput[];
|
|
252
267
|
version?: number;
|
|
253
268
|
}
|
|
254
269
|
export type LoopTemplateKind = "workflow" | "loop";
|
|
@@ -367,7 +382,7 @@ export interface CreateLoopInput {
|
|
|
367
382
|
name: string;
|
|
368
383
|
description?: string;
|
|
369
384
|
schedule: ScheduleSpec;
|
|
370
|
-
target:
|
|
385
|
+
target: LoopTargetInput;
|
|
371
386
|
goal?: GoalSpec;
|
|
372
387
|
machine?: LoopMachineRef;
|
|
373
388
|
catchUp?: CatchUpPolicy;
|
package/docs/USAGE.md
CHANGED
|
@@ -148,6 +148,56 @@ accounts tools add codewith --label "Codewith" --env-var CODEWITH_HOME --bin cod
|
|
|
148
148
|
accounts tools add aicopilot --label "AI Copilot" --env-var AICOPILOT_CONFIG_DIR --bin aicopilot
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
+
## Prompt Files
|
|
152
|
+
|
|
153
|
+
Use prompt files for production agent prompts instead of long inline strings.
|
|
154
|
+
This keeps the durable prompt source reviewable and prevents shell history from
|
|
155
|
+
becoming the only copy of the prompt.
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
mkdir -p ~/.hasna/loops/prompts
|
|
159
|
+
$EDITOR ~/.hasna/loops/prompts/repo-morning-check.md
|
|
160
|
+
|
|
161
|
+
loops create agent morning-check \
|
|
162
|
+
--provider codewith \
|
|
163
|
+
--auth-profile account001 \
|
|
164
|
+
--cron "0 8 * * *" \
|
|
165
|
+
--cwd /path/to/repo \
|
|
166
|
+
--prompt-file ~/.hasna/loops/prompts/repo-morning-check.md
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Workflow JSON supports `promptFile` on agent targets. Relative paths resolve
|
|
170
|
+
from the workflow JSON file's directory:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"name": "repo-morning",
|
|
175
|
+
"steps": [
|
|
176
|
+
{
|
|
177
|
+
"id": "review",
|
|
178
|
+
"target": {
|
|
179
|
+
"type": "agent",
|
|
180
|
+
"provider": "codewith",
|
|
181
|
+
"cwd": "/path/to/repo",
|
|
182
|
+
"promptFile": "prompts/repo-morning-review.md"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
OpenLoops stores the resolved prompt text for execution and records
|
|
190
|
+
`promptSource` metadata with the absolute file path. Public CLI output from
|
|
191
|
+
`show`, `list`, `workflows list`, `workflows show`, `workflows validate`,
|
|
192
|
+
`workflows create`, and `templates render` redacts prompt bodies by default
|
|
193
|
+
while keeping `promptSource` visible. Use
|
|
194
|
+
`~/.hasna/loops/prompts/<stable-name>.md` as the default prompt store for local
|
|
195
|
+
production loops.
|
|
196
|
+
|
|
197
|
+
Reusable custom templates cannot contain `promptFile`. Keep prompt files in
|
|
198
|
+
direct workflow JSON or agent loop creation; use template variables only for
|
|
199
|
+
non-secret routing/configuration data.
|
|
200
|
+
|
|
151
201
|
## Workflows
|
|
152
202
|
|
|
153
203
|
Create a workflow JSON file:
|
package/package.json
CHANGED