pi-feats 0.1.2 → 0.1.4
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 +24 -1
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/assets/pi-feats-preview.png +0 -0
- package/extensions/api-server/application-log-store.ts +31 -0
- package/extensions/api-server/application-runtime.ts +80 -18
- package/extensions/api-server/application-store.ts +6 -2
- package/extensions/api-server/profile-store.ts +47 -20
- package/extensions/api-server/server.ts +182 -47
- package/extensions/browser-harness/cdp/commands.ts +260 -0
- package/extensions/browser-harness/cdp/console-buffer.ts +188 -0
- package/extensions/browser-harness/cdp/discovery.ts +198 -0
- package/extensions/browser-harness/cdp/errors.ts +23 -0
- package/extensions/browser-harness/cdp/events.ts +170 -0
- package/extensions/browser-harness/cdp/network-buffer.ts +146 -0
- package/extensions/browser-harness/cdp/ownership.ts +73 -0
- package/extensions/browser-harness/cdp/record-store.ts +61 -0
- package/extensions/browser-harness/cdp/session.ts +403 -0
- package/extensions/browser-harness/cdp/steel-transport.ts +151 -0
- package/extensions/browser-harness/cdp/target-factory.ts +153 -0
- package/extensions/browser-harness/cdp/types.ts +82 -0
- package/extensions/browser-harness/client.ts +366 -0
- package/extensions/browser-harness/daemon/bridge.ts +531 -0
- package/extensions/browser-harness/daemon/index.ts +86 -0
- package/extensions/browser-harness/daemon/protocol.ts +110 -0
- package/extensions/browser-harness/daemon/queue.ts +85 -0
- package/extensions/browser-harness/daemon/server.ts +170 -0
- package/extensions/browser-harness/daemon/spawn.ts +124 -0
- package/extensions/browser-harness/daemon/transport.ts +178 -0
- package/extensions/browser-harness/domains/ax-tree.ts +179 -0
- package/extensions/browser-harness/domains/box.ts +51 -0
- package/extensions/browser-harness/domains/cdp-call.ts +55 -0
- package/extensions/browser-harness/domains/click.ts +97 -0
- package/extensions/browser-harness/domains/console.ts +219 -0
- package/extensions/browser-harness/domains/dialog.ts +34 -0
- package/extensions/browser-harness/domains/drag.ts +67 -0
- package/extensions/browser-harness/domains/element-call.ts +117 -0
- package/extensions/browser-harness/domains/files.ts +219 -0
- package/extensions/browser-harness/domains/fill-engine.ts +80 -0
- package/extensions/browser-harness/domains/form-batch.ts +65 -0
- package/extensions/browser-harness/domains/form.ts +283 -0
- package/extensions/browser-harness/domains/headers.ts +89 -0
- package/extensions/browser-harness/domains/history.ts +80 -0
- package/extensions/browser-harness/domains/isolated-tab.ts +91 -0
- package/extensions/browser-harness/domains/js.ts +241 -0
- package/extensions/browser-harness/domains/keyboard.ts +197 -0
- package/extensions/browser-harness/domains/navigate.ts +161 -0
- package/extensions/browser-harness/domains/network.ts +301 -0
- package/extensions/browser-harness/domains/page.ts +154 -0
- package/extensions/browser-harness/domains/readpage/read-page.ts +174 -0
- package/extensions/browser-harness/domains/readpage/readability.ts +65 -0
- package/extensions/browser-harness/domains/ref-resolve.ts +118 -0
- package/extensions/browser-harness/domains/render.ts +43 -0
- package/extensions/browser-harness/domains/screenshot-capture.ts +80 -0
- package/extensions/browser-harness/domains/screenshot.ts +76 -0
- package/extensions/browser-harness/domains/scroll.ts +59 -0
- package/extensions/browser-harness/domains/search/google-serp.ts +85 -0
- package/extensions/browser-harness/domains/search/web-search.ts +144 -0
- package/extensions/browser-harness/domains/setup.ts +35 -0
- package/extensions/browser-harness/domains/snapshot.ts +256 -0
- package/extensions/browser-harness/domains/tabs.ts +202 -0
- package/extensions/browser-harness/domains/target.ts +35 -0
- package/extensions/browser-harness/domains/viewport.ts +42 -0
- package/extensions/browser-harness/index.ts +88 -0
- package/extensions/browser-harness/profile/bind.ts +90 -0
- package/extensions/browser-harness/profile/browser-process.ts +320 -0
- package/extensions/browser-harness/profile/command.ts +189 -0
- package/extensions/browser-harness/profile/launch.ts +80 -0
- package/extensions/browser-harness/profile/list.ts +131 -0
- package/extensions/browser-harness/profile/paths.ts +103 -0
- package/extensions/browser-harness/profile/store.ts +84 -0
- package/extensions/browser-harness/prompt.ts +16 -0
- package/extensions/browser-harness/registry.ts +80 -0
- package/extensions/browser-harness/schemas/common.ts +11 -0
- package/extensions/browser-harness/schemas/parse.ts +23 -0
- package/extensions/browser-harness/setup.ts +47 -0
- package/extensions/browser-harness/state.ts +51 -0
- package/extensions/browser-harness/steel.ts +215 -0
- package/extensions/browser-harness/util/debug.ts +8 -0
- package/extensions/browser-harness/util/guards.ts +30 -0
- package/extensions/browser-harness/util/js-template.ts +12 -0
- package/extensions/browser-harness/util/mutex.ts +20 -0
- package/extensions/browser-harness/util/paths.ts +31 -0
- package/extensions/browser-harness/util/result.ts +9 -0
- package/extensions/browser-harness/util/sharp-shim.ts +42 -0
- package/extensions/browser-harness/util/tool.ts +185 -0
- package/extensions/browser-harness/util/truncate.ts +41 -0
- package/extensions/cli-resources.ts +35 -16
- package/extensions/composio/index.ts +163 -0
- package/extensions/context-memory/index.ts +58 -0
- package/extensions/lib/application-context.ts +22 -0
- package/extensions/lib/application-handler-templates.ts +17 -1
- package/extensions/lib/context-memory.ts +165 -0
- package/extensions/lib/profile-sandbox.ts +3 -0
- package/extensions/lib/workflow-harness.ts +238 -0
- package/extensions/pi-console-webui/app/[section]/page.tsx +6 -1
- package/extensions/pi-console-webui/app/applications/[slug]/page.tsx +15 -0
- package/extensions/pi-console-webui/app/layout.tsx +6 -1
- package/extensions/pi-console-webui/app/login/page.tsx +5 -9
- package/extensions/pi-console-webui/app/not-found.tsx +5 -0
- package/extensions/pi-console-webui/app/page.tsx +6 -0
- package/extensions/pi-console-webui/app/terminal/page.tsx +3 -0
- package/extensions/pi-console-webui/components/application-handler-editor.tsx +4 -4
- package/extensions/pi-console-webui/components/application-logs.tsx +12 -8
- package/extensions/pi-console-webui/components/application-settings.tsx +5 -1
- package/extensions/pi-console-webui/components/chat-workspace.tsx +7 -4
- package/extensions/pi-console-webui/components/console-page.tsx +3 -1
- package/extensions/pi-console-webui/components/console.tsx +53 -7
- package/extensions/pi-console-webui/components/login-form.tsx +14 -0
- package/extensions/pi-console-webui/components/pulse-resources.tsx +10 -6
- package/extensions/pi-console-webui/components/skill-resources.tsx +3 -3
- package/extensions/pi-console-webui/lib/page-metadata.ts +33 -0
- package/extensions/profiles.ts +18 -7
- package/extensions/pulse/store.ts +5 -2
- package/extensions/sequential-workflow.ts +340 -256
- package/extensions/skill-sources/store.ts +24 -3
- package/licenses/pi-browser-harness-MIT.txt +21 -0
- package/package.json +25 -4
- package/schemas/sequential-workflow-template.schema.json +62 -0
- package/skills/browser-harness/SKILL.md +33 -0
- package/skills/composio-connections/SKILL.md +75 -0
- package/skills/sequential-workflow/SKILL.md +70 -0
- package/skills/sequential-workflow-template/SKILL.md +44 -0
|
@@ -1,270 +1,354 @@
|
|
|
1
|
+
import { workflowHarness } from "./lib/workflow-harness.ts";
|
|
1
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
3
4
|
import { Type } from "typebox";
|
|
4
|
-
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { DatabaseSync, type SQLInputValue } from "node:sqlite";
|
|
6
|
+
import { mkdir, stat, readFile } from "node:fs/promises";
|
|
5
7
|
import { homedir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
|
|
8
|
-
type TaskType = "action" | "collect" | "evaluate";
|
|
9
|
-
type TaskStatus = "pending" | "running" | "awaiting_user" | "evaluating" | "accepted" | "rejected" | "failed";
|
|
8
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
type TaskType = "action" | "collect" | "evaluate" | "workflow";
|
|
11
|
+
type TaskStatus = "pending" | "running" | "awaiting_user" | "waiting_subworkflow" | "evaluating" | "accepted" | "rejected" | "failed";
|
|
10
12
|
type WorkflowStatus = "running" | "awaiting_user" | "evaluating" | "completed" | "cancelled" | "failed";
|
|
11
|
-
|
|
12
13
|
type Task = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
id: number;
|
|
15
|
+
workflow_id: number;
|
|
16
|
+
position: number;
|
|
17
|
+
type: TaskType;
|
|
18
|
+
instruction: string;
|
|
19
|
+
criteria: string | null;
|
|
20
|
+
status: TaskStatus;
|
|
21
|
+
attempts: number;
|
|
22
|
+
result: string | null;
|
|
23
|
+
evaluation: string | null;
|
|
24
|
+
child_workflow_id: number | null;
|
|
23
25
|
};
|
|
24
|
-
|
|
25
26
|
type Workflow = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
id: number;
|
|
28
|
+
title: string;
|
|
29
|
+
source: string;
|
|
30
|
+
status: WorkflowStatus;
|
|
31
|
+
parent_workflow_id: number | null;
|
|
30
32
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
type WorkflowTaskDefinition = {
|
|
34
|
+
type: TaskType;
|
|
35
|
+
instruction: string;
|
|
36
|
+
criteria?: string;
|
|
37
|
+
};
|
|
38
|
+
type WorkflowDefinition = {
|
|
39
|
+
title: string;
|
|
40
|
+
source: string;
|
|
41
|
+
tasks: WorkflowTaskDefinition[];
|
|
42
|
+
};
|
|
43
|
+
type WorkflowTemplate = WorkflowDefinition & {
|
|
44
|
+
version: 1;
|
|
45
|
+
};
|
|
46
|
+
const templateMaxBytes = 1024 * 1024;
|
|
47
|
+
const templateMaxTasks = 1000;
|
|
48
|
+
const templateMaxTitleLength = 1000;
|
|
49
|
+
const templateMaxSourceLength = 2000;
|
|
50
|
+
const templateMaxTextLength = 10000;
|
|
51
|
+
const taskType = StringEnum(["action", "collect", "evaluate", "workflow"] as const);
|
|
33
52
|
const phaseType = StringEnum(["action", "collect"] as const);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
criteria: task.criteria,
|
|
95
|
-
status: task.status,
|
|
96
|
-
attempts: task.attempts,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const activateNext = (workflow: Workflow) => {
|
|
100
|
-
const next = currentTask(workflow.id);
|
|
101
|
-
if (!next) {
|
|
102
|
-
db.prepare("UPDATE workflows SET status = 'completed', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(workflow.id);
|
|
103
|
-
event(workflow.id, null, "completed", { message: "Todas as tasks foram aceitas." });
|
|
104
|
-
return { completed: true };
|
|
53
|
+
const workflowRoot = () => process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
54
|
+
const workflowDatabase = () => join(workflowRoot(), "sequential-workflow.db");
|
|
55
|
+
const templateDirectory = () => join(workflowRoot(), "sequential_workflow_templates");
|
|
56
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
57
|
+
const requiredText = (value: unknown, field: string, maxLength: number) => {
|
|
58
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
59
|
+
throw new Error(`Template inválido: ${field} é obrigatório.`);
|
|
60
|
+
if (value.length > maxLength)
|
|
61
|
+
throw new Error(`Template inválido: ${field} excede ${maxLength} caracteres.`);
|
|
62
|
+
return value;
|
|
63
|
+
};
|
|
64
|
+
const onlyProperties = (value: Record<string, unknown>, allowed: string[], field: string) => {
|
|
65
|
+
const unexpected = Object.keys(value).find((key) => !allowed.includes(key));
|
|
66
|
+
if (unexpected)
|
|
67
|
+
throw new Error(`Template inválido: ${field} contém a propriedade não permitida ${unexpected}.`);
|
|
68
|
+
};
|
|
69
|
+
const validateTemplate = (value: unknown): WorkflowTemplate => {
|
|
70
|
+
if (!isRecord(value))
|
|
71
|
+
throw new Error("Template inválido: a raiz deve ser um objeto JSON.");
|
|
72
|
+
onlyProperties(value, ["version", "title", "source", "tasks"], "raiz");
|
|
73
|
+
if (value.version !== 1)
|
|
74
|
+
throw new Error("Template inválido: version deve ser 1.");
|
|
75
|
+
if (!Array.isArray(value.tasks) || value.tasks.length === 0)
|
|
76
|
+
throw new Error("Template inválido: tasks deve conter ao menos uma task.");
|
|
77
|
+
if (value.tasks.length > templateMaxTasks)
|
|
78
|
+
throw new Error(`Template inválido: tasks não pode exceder ${templateMaxTasks} itens.`);
|
|
79
|
+
const tasks = value.tasks.map((item, index) => {
|
|
80
|
+
if (!isRecord(item))
|
|
81
|
+
throw new Error(`Template inválido: tasks[${index}] deve ser um objeto.`);
|
|
82
|
+
onlyProperties(item, ["type", "instruction", "criteria"], `tasks[${index}]`);
|
|
83
|
+
if (item.type !== "action" && item.type !== "collect" && item.type !== "evaluate" && item.type !== "workflow")
|
|
84
|
+
throw new Error(`Template inválido: tasks[${index}].type deve ser action, collect, evaluate ou workflow.`);
|
|
85
|
+
const criteria = item.criteria === undefined ? undefined : requiredText(item.criteria, `tasks[${index}].criteria`, templateMaxTextLength);
|
|
86
|
+
if (item.type === "collect" && !criteria)
|
|
87
|
+
throw new Error(`Template inválido: tasks[${index}] é Collect e exige criteria.`);
|
|
88
|
+
return { type: item.type as TaskType, instruction: requiredText(item.instruction, `tasks[${index}].instruction`, templateMaxTextLength), criteria };
|
|
89
|
+
});
|
|
90
|
+
return { version: 1, title: requiredText(value.title, "title", templateMaxTitleLength), source: requiredText(value.source, "source", templateMaxSourceLength), tasks };
|
|
91
|
+
};
|
|
92
|
+
const resolveTemplatePath = (inputPath: string) => {
|
|
93
|
+
const expanded = inputPath === "~" ? homedir() : inputPath.startsWith("~/") ? join(homedir(), inputPath.slice(2)) : inputPath;
|
|
94
|
+
return isAbsolute(expanded) || expanded === "." || expanded === ".." || expanded.startsWith("./") || expanded.startsWith("../") || expanded.startsWith(".\\") || expanded.startsWith("..\\") ? resolve(process.cwd(), expanded) : resolve(templateDirectory(), expanded);
|
|
95
|
+
};
|
|
96
|
+
const loadTemplate = async (inputPath: string) => {
|
|
97
|
+
const path = resolveTemplatePath(!inputPath.includes("/") && !inputPath.includes("\\") && !inputPath.endsWith(".json") ? `${inputPath}.json` : inputPath);
|
|
98
|
+
let content: string;
|
|
99
|
+
try {
|
|
100
|
+
const info = await stat(path);
|
|
101
|
+
if (!info.isFile())
|
|
102
|
+
throw new Error("não é um arquivo regular");
|
|
103
|
+
if (info.size > templateMaxBytes)
|
|
104
|
+
throw new Error(`excede o limite de ${templateMaxBytes} bytes`);
|
|
105
|
+
content = await readFile(path, "utf8");
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new Error(`Não foi possível ler o template ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
109
|
+
}
|
|
110
|
+
let document: unknown;
|
|
111
|
+
try {
|
|
112
|
+
document = JSON.parse(content);
|
|
105
113
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw new Error(`Template inválido: JSON malformado em ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
116
|
+
}
|
|
117
|
+
return { path, hash: createHash("sha256").update(content).digest("hex"), template: validateTemplate(document) };
|
|
118
|
+
};
|
|
119
|
+
export default function (host: ExtensionAPI) {
|
|
120
|
+
const db = new DatabaseSync(workflowDatabase());
|
|
121
|
+
db.exec("PRAGMA foreign_keys = ON; PRAGMA journal_mode = WAL;");
|
|
122
|
+
db.exec(`CREATE TABLE IF NOT EXISTS workflows (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, source TEXT NOT NULL, status TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);
|
|
123
|
+
CREATE TABLE IF NOT EXISTS workflow_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT, workflow_id INTEGER NOT NULL REFERENCES workflows(id), position INTEGER NOT NULL, type TEXT NOT NULL, instruction TEXT NOT NULL, criteria TEXT, status TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, result TEXT, evaluation TEXT, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(workflow_id, position));
|
|
124
|
+
CREATE TABLE IF NOT EXISTS workflow_events (id INTEGER PRIMARY KEY AUTOINCREMENT, workflow_id INTEGER NOT NULL REFERENCES workflows(id), task_id INTEGER REFERENCES workflow_tasks(id), phase TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP);`);
|
|
125
|
+
const columns = (table: string) => db.prepare(`PRAGMA table_info(${table})`).all() as Array<{
|
|
126
|
+
name: string;
|
|
127
|
+
}>;
|
|
128
|
+
if (!columns("workflows").some((column) => column.name === "parent_workflow_id"))
|
|
129
|
+
db.exec("ALTER TABLE workflows ADD COLUMN parent_workflow_id INTEGER REFERENCES workflows(id)");
|
|
130
|
+
if (!columns("workflow_tasks").some((column) => column.name === "child_workflow_id"))
|
|
131
|
+
db.exec("ALTER TABLE workflow_tasks ADD COLUMN child_workflow_id INTEGER REFERENCES workflows(id)");
|
|
132
|
+
db.exec("CREATE INDEX IF NOT EXISTS workflows_parent_workflow_id_idx ON workflows(parent_workflow_id); CREATE INDEX IF NOT EXISTS workflow_tasks_child_workflow_id_idx ON workflow_tasks(child_workflow_id); CREATE UNIQUE INDEX IF NOT EXISTS workflow_tasks_unique_child_workflow_idx ON workflow_tasks(child_workflow_id) WHERE child_workflow_id IS NOT NULL;");
|
|
133
|
+
const harness = workflowHarness(host, db);
|
|
134
|
+
const pi = harness.api;
|
|
135
|
+
const one = <T>(sql: string, ...params: SQLInputValue[]) => db.prepare(sql).get(...params) as T | undefined;
|
|
136
|
+
const many = <T>(sql: string, ...params: SQLInputValue[]) => db.prepare(sql).all(...params) as T[];
|
|
137
|
+
const event = (workflowId: number, taskId: number | null, phase: string, payload: unknown) => db.prepare("INSERT INTO workflow_events (workflow_id, task_id, phase, payload) VALUES (?, ?, ?, ?)").run(workflowId, taskId, phase, JSON.stringify(payload));
|
|
138
|
+
const workflow = (id: number) => one<Workflow>("SELECT id, title, source, status, parent_workflow_id FROM workflows WHERE id = ?", id);
|
|
139
|
+
const isActive = (item: Workflow) => ["running", "awaiting_user", "evaluating"].includes(item.status);
|
|
140
|
+
const currentTask = (workflowId: number) => one<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE workflow_id = ? AND status NOT IN ('accepted', 'failed') ORDER BY position LIMIT 1", workflowId);
|
|
141
|
+
const taskSummary = (task: Task) => ({ id: task.id, position: task.position, type: task.type, instruction: task.instruction, criteria: task.criteria, status: task.status, attempts: task.attempts, childWorkflowId: task.child_workflow_id });
|
|
142
|
+
const requireActiveWorkflow = (id: number) => { const item = workflow(id); if (!item)
|
|
143
|
+
throw new Error(`Workflow #${id} não existe.`); if (!isActive(item))
|
|
144
|
+
throw new Error(`Workflow #${id} está ${item.status} e não pode ser alterado.`); return item; };
|
|
145
|
+
const resolveParentAfterChild = (child: Workflow) => {
|
|
146
|
+
const parentTask = one<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE child_workflow_id = ? AND status = 'waiting_subworkflow'", child.id);
|
|
147
|
+
if (!parentTask)
|
|
148
|
+
return;
|
|
149
|
+
const parent = workflow(parentTask.workflow_id);
|
|
150
|
+
if (!parent || !isActive(parent))
|
|
151
|
+
return;
|
|
152
|
+
const result = `Subworkflow #${child.id} (${child.title}) terminou com status ${child.status}.`;
|
|
153
|
+
if (child.status === "completed" && !parentTask.criteria) {
|
|
154
|
+
db.prepare("UPDATE workflow_tasks SET result = ?, status = 'accepted', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(result, parentTask.id);
|
|
155
|
+
event(parent.id, parentTask.id, "subworkflow_completed", { childWorkflowId: child.id, accepted: true });
|
|
156
|
+
activateNext(parent);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
db.prepare("UPDATE workflow_tasks SET result = ?, status = 'evaluating', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(result, parentTask.id);
|
|
160
|
+
db.prepare("UPDATE workflows SET status = 'evaluating', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(parent.id);
|
|
161
|
+
event(parent.id, parentTask.id, child.status === "completed" ? "subworkflow_completed" : "subworkflow_terminal", { childWorkflowId: child.id, status: child.status });
|
|
162
|
+
};
|
|
163
|
+
const activateNext = (item: Workflow): {
|
|
164
|
+
completed: boolean;
|
|
165
|
+
task?: Task;
|
|
166
|
+
} => {
|
|
167
|
+
const next = currentTask(item.id);
|
|
168
|
+
if (!next) {
|
|
169
|
+
db.prepare("UPDATE workflows SET status = 'completed', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(item.id);
|
|
170
|
+
const completed = workflow(item.id)!;
|
|
171
|
+
event(item.id, null, "completed", { message: "Todas as tasks foram aceitas." });
|
|
172
|
+
resolveParentAfterChild(completed);
|
|
173
|
+
return { completed: true };
|
|
174
|
+
}
|
|
175
|
+
const status: TaskStatus = next.type === "collect" ? "awaiting_user" : "running";
|
|
176
|
+
const workflowStatus: WorkflowStatus = next.type === "collect" ? "awaiting_user" : "running";
|
|
177
|
+
db.prepare("UPDATE workflow_tasks SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(status, next.id);
|
|
178
|
+
db.prepare("UPDATE workflows SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(workflowStatus, item.id);
|
|
179
|
+
const activated = currentTask(item.id)!;
|
|
180
|
+
event(item.id, activated.id, "activated", taskSummary(activated));
|
|
181
|
+
return { completed: false, task: activated };
|
|
182
|
+
};
|
|
183
|
+
const cancelWorkflow = (workflowId: number) => {
|
|
184
|
+
const item = requireActiveWorkflow(workflowId);
|
|
185
|
+
db.prepare("UPDATE workflows SET status='cancelled', updated_at=CURRENT_TIMESTAMP WHERE id=?").run(item.id);
|
|
186
|
+
event(item.id, null, "cancelled", { by: "user", sessionId: harness.session() });
|
|
187
|
+
resolveParentAfterChild(workflow(item.id)!);
|
|
188
|
+
return { content: [{ type: "text" as const, text: `Workflow #${item.id} cancelado.` }], details: { workflowId: item.id, status: "cancelled" } };
|
|
126
189
|
};
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}),
|
|
147
|
-
async execute(_id, params) {
|
|
148
|
-
const active = activeWorkflow();
|
|
149
|
-
if (active) throw new Error(`Já existe um workflow ativo (#${active.id}: ${active.title}). Conclua ou cancele-o antes de criar outro.`);
|
|
150
|
-
for (const [index, task] of params.tasks.entries()) {
|
|
151
|
-
if (task.type === "collect" && !task.criteria) {
|
|
152
|
-
throw new Error(`Task ${index + 1} é Collect e exige criteria.`);
|
|
190
|
+
const createWorkflow = (definition: WorkflowDefinition, parentWorkflowId?: number, parentTaskId?: number) => {
|
|
191
|
+
for (const [index, task] of definition.tasks.entries())
|
|
192
|
+
if (task.type === "collect" && !task.criteria)
|
|
193
|
+
throw new Error(`Task ${index + 1} é Collect e exige criteria.`);
|
|
194
|
+
let parentTask: Task | undefined;
|
|
195
|
+
if (parentTaskId !== undefined) {
|
|
196
|
+
parentTask = one<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE id = ?", parentTaskId);
|
|
197
|
+
if (!parentTask)
|
|
198
|
+
throw new Error(`Task pai #${parentTaskId} não existe.`);
|
|
199
|
+
if (parentTask.type !== "workflow")
|
|
200
|
+
throw new Error(`Task pai #${parentTaskId} deve ser do tipo workflow.`);
|
|
201
|
+
if (parentTask.status !== "running")
|
|
202
|
+
throw new Error(`Task pai #${parentTaskId} não está pronta para iniciar um subworkflow.`);
|
|
203
|
+
if (parentTask.child_workflow_id !== null)
|
|
204
|
+
throw new Error(`Task pai #${parentTaskId} já possui um subworkflow vinculado.`);
|
|
205
|
+
if (parentWorkflowId !== undefined && parentWorkflowId !== parentTask.workflow_id)
|
|
206
|
+
throw new Error("parentWorkflowId não corresponde ao workflow da parentTaskId.");
|
|
207
|
+
parentWorkflowId = parentTask.workflow_id;
|
|
208
|
+
requireActiveWorkflow(parentWorkflowId);
|
|
153
209
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
name: "
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
210
|
+
else if (parentWorkflowId !== undefined && !workflow(parentWorkflowId))
|
|
211
|
+
throw new Error(`Workflow pai #${parentWorkflowId} não existe.`);
|
|
212
|
+
{
|
|
213
|
+
const inserted = db.prepare("INSERT INTO workflows (title, source, status, parent_workflow_id, session_id) VALUES (?, ?, 'running', ?, ?)").run(definition.title, definition.source, parentWorkflowId ?? null, harness.session());
|
|
214
|
+
const workflowId = Number(inserted.lastInsertRowid);
|
|
215
|
+
const insertTask = db.prepare("INSERT INTO workflow_tasks (workflow_id, position, type, instruction, criteria, status) VALUES (?, ?, ?, ?, ?, 'pending')");
|
|
216
|
+
definition.tasks.forEach((task, index) => insertTask.run(workflowId, index + 1, task.type, task.instruction, task.criteria ?? null));
|
|
217
|
+
event(workflowId, null, "created", { title: definition.title, taskCount: definition.tasks.length, parentWorkflowId: parentWorkflowId ?? null });
|
|
218
|
+
if (parentTask) {
|
|
219
|
+
db.prepare("UPDATE workflow_tasks SET child_workflow_id = ?, status = 'waiting_subworkflow', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(workflowId, parentTask.id);
|
|
220
|
+
event(parentTask.workflow_id, parentTask.id, "child_linked", { childWorkflowId: workflowId });
|
|
221
|
+
}
|
|
222
|
+
const next = activateNext(workflow(workflowId)!);
|
|
223
|
+
const suffix = parentTask ? ` vinculado à task pai #${parentTask.id}.` : ".";
|
|
224
|
+
return { content: [{ type: "text" as const, text: next.completed ? `Workflow #${workflowId} criado e concluído${suffix}` : `Workflow #${workflowId} criado${suffix} Execute somente a task #${next.task!.position}: ${next.task!.instruction}` }], details: { workflowId, parentWorkflowId: parentWorkflowId ?? null, parentTaskId: parentTask?.id ?? null, next } };
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
pi.registerTool({ name: "sequential_workflow_create", label: "Create Sequential Workflow", description: "Persiste um workflow e ativa sua primeira task, sem bloquear outros workflows.", promptSnippet: "Create a persisted sequential workflow", promptGuidelines: ["Use sequential_workflow_create only after the user explicitly names Sequential Workflow, sequential-workflow, workflow sequencial, or fluxo de trabalho sequencial and asks to create or execute it.", "A Collect task passed to sequential_workflow_create must include acceptance criteria."], parameters: Type.Object({ title: Type.String({ minLength: 1 }), source: Type.String({ minLength: 1 }), parentWorkflowId: Type.Optional(Type.Integer({ minimum: 1 })), parentTaskId: Type.Optional(Type.Integer({ minimum: 1 })), tasks: Type.Array(Type.Object({ type: taskType, instruction: Type.String({ minLength: 1 }), criteria: Type.Optional(Type.String({ minLength: 1 })) }), { minItems: 1 }) }), async execute(_id, params): Promise<{
|
|
228
|
+
content: {
|
|
229
|
+
type: "text";
|
|
230
|
+
text: string;
|
|
231
|
+
}[];
|
|
232
|
+
details: Record<string, unknown>;
|
|
233
|
+
}> { return createWorkflow(params, params.parentWorkflowId, params.parentTaskId); } });
|
|
234
|
+
pi.registerTool({ name: "sequential_workflow_prepare_template_directory", label: "Prepare Sequential Workflow Template Directory", description: "Cria, quando necessário, e informa o diretório padrão de templates JSON de Sequential Workflow do profile ativo.", promptSnippet: "Prepare the active profile's default Sequential Workflow template directory", promptGuidelines: ["Use sequential_workflow_prepare_template_directory only when the user explicitly names Sequential Workflow and asks to create or edit its JSON template without specifying an output directory."], parameters: Type.Object({}), async execute() { const path = templateDirectory(); await mkdir(path, { recursive: true }); return { content: [{ type: "text", text: `Diretório de templates pronto: ${path}` }], details: { path } }; } });
|
|
235
|
+
pi.registerTool({ name: "sequential_workflow_validate_template", label: "Validate Sequential Workflow Template", description: "Lê e valida um arquivo JSON de template de Sequential Workflow sem criar ou executar um workflow.", promptSnippet: "Validate a Sequential Workflow JSON template before it is used", promptGuidelines: ["Use sequential_workflow_validate_template only when the user explicitly names Sequential Workflow and asks to create, edit, or validate its JSON template."], parameters: Type.Object({ path: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
|
|
236
|
+
content: {
|
|
237
|
+
type: "text";
|
|
238
|
+
text: string;
|
|
239
|
+
}[];
|
|
240
|
+
details: Record<string, unknown>;
|
|
241
|
+
}> { const loaded = await loadTemplate(params.path); return { content: [{ type: "text", text: `Template válido: ${loaded.path} (${loaded.template.tasks.length} tasks, SHA-256: ${loaded.hash}).` }], details: loaded }; } });
|
|
242
|
+
pi.registerTool({ name: "sequential_workflow_create_from_template", label: "Create Sequential Workflow from Template", description: "Lê um template JSON validado e cria um workflow.", promptSnippet: "Create and start a persisted Sequential Workflow from a validated JSON template", promptGuidelines: ["Use sequential_workflow_create_from_template only when the user explicitly names Sequential Workflow and asks to execute a JSON template."], parameters: Type.Object({ path: Type.String({ minLength: 1 }), parentWorkflowId: Type.Optional(Type.Integer({ minimum: 1 })), parentTaskId: Type.Optional(Type.Integer({ minimum: 1 })) }), async execute(_id, params): Promise<{
|
|
243
|
+
content: {
|
|
244
|
+
type: "text";
|
|
245
|
+
text: string;
|
|
246
|
+
}[];
|
|
247
|
+
details: Record<string, unknown>;
|
|
248
|
+
}> { const loaded = await loadTemplate(params.path); return createWorkflow({ title: loaded.template.title, source: `Template ${loaded.path} (SHA-256: ${loaded.hash}): ${loaded.template.source}`, tasks: loaded.template.tasks }, params.parentWorkflowId, params.parentTaskId); } });
|
|
249
|
+
pi.registerTool({ name: "sequential_workflow_record_result", label: "Record Workflow Result", description: "Registra o resultado da Action ou Collect atual de um workflow identificado.", promptSnippet: "Record the result of the active Action or Collect task", promptGuidelines: ["Use sequential_workflow_record_result immediately after completing the active Action or receiving the active Collect response."], parameters: Type.Object({ workflowId: Type.Integer({ minimum: 1 }), taskId: Type.Integer({ minimum: 1 }), phase: phaseType, result: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
|
|
250
|
+
content: {
|
|
251
|
+
type: "text";
|
|
252
|
+
text: string;
|
|
253
|
+
}[];
|
|
254
|
+
details: Record<string, unknown>;
|
|
255
|
+
}> {
|
|
256
|
+
const item = requireActiveWorkflow(params.workflowId);
|
|
257
|
+
const task = currentTask(item.id);
|
|
258
|
+
if (!task)
|
|
259
|
+
throw new Error("Não existe task pendente.");
|
|
260
|
+
if (task.type !== params.phase)
|
|
261
|
+
throw new Error(`A task atual é ${task.type}, não ${params.phase}.`);
|
|
262
|
+
if (task.status === "evaluating")
|
|
263
|
+
throw new Error("O resultado já foi registrado; avalie a task atual.");
|
|
264
|
+
if (task.criteria) {
|
|
265
|
+
db.prepare("UPDATE workflow_tasks SET result = ?, status = 'evaluating', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(params.result, task.id);
|
|
266
|
+
db.prepare("UPDATE workflows SET status = 'evaluating', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(item.id);
|
|
267
|
+
event(item.id, task.id, "result_recorded", { phase: params.phase, result: params.result });
|
|
268
|
+
return { content: [{ type: "text", text: `Resultado registrado. Agora avalie a task #${task.position} contra o critério.` }], details: { taskId: task.id, needsEvaluation: true } };
|
|
269
|
+
}
|
|
270
|
+
db.prepare("UPDATE workflow_tasks SET result = ?, status = 'accepted', updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(params.result, task.id);
|
|
271
|
+
event(item.id, task.id, "accepted_without_criteria", { result: params.result });
|
|
272
|
+
const next = activateNext(item);
|
|
273
|
+
return { content: [{ type: "text", text: next.completed ? "Task aceita; workflow concluído." : `Task aceita. Execute a task #${next.task!.position}: ${next.task!.instruction}` }], details: { next } };
|
|
274
|
+
} });
|
|
275
|
+
pi.registerTool({ name: "sequential_workflow_evaluate", label: "Evaluate Workflow Task", description: "Registra a aceitação ou reprovação da task atual de um workflow identificado.", promptSnippet: "Accept or reject the active workflow task after evaluation", promptGuidelines: ["Use sequential_workflow_evaluate after every task with criteria and for every Evaluate task; never advance a rejected task."], parameters: Type.Object({ workflowId: Type.Integer({ minimum: 1 }), taskId: Type.Integer({ minimum: 1 }), accepted: Type.Boolean(), reasoning: Type.String({ minLength: 1 }) }), async execute(_id, params): Promise<{
|
|
276
|
+
content: {
|
|
277
|
+
type: "text";
|
|
278
|
+
text: string;
|
|
279
|
+
}[];
|
|
280
|
+
details: Record<string, unknown>;
|
|
281
|
+
}> {
|
|
282
|
+
const item = requireActiveWorkflow(params.workflowId);
|
|
283
|
+
const task = currentTask(item.id);
|
|
284
|
+
if (!task)
|
|
285
|
+
throw new Error("Não existe task pendente.");
|
|
286
|
+
if (task.type !== "evaluate" && task.status !== "evaluating")
|
|
287
|
+
throw new Error("Registre o resultado da task antes de avaliá-la.");
|
|
288
|
+
if (params.accepted) {
|
|
289
|
+
db.prepare("UPDATE workflow_tasks SET status = 'accepted', evaluation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(params.reasoning, task.id);
|
|
290
|
+
event(item.id, task.id, "accepted", { reasoning: params.reasoning });
|
|
291
|
+
const next = activateNext(item);
|
|
292
|
+
return { content: [{ type: "text", text: next.completed ? "Avaliação aceita; workflow concluído." : `Avaliação aceita. Execute somente a task #${next.task!.position}: ${next.task!.instruction}` }], details: { accepted: true, next } };
|
|
293
|
+
}
|
|
294
|
+
if (task.type === "workflow") {
|
|
295
|
+
event(item.id, task.id, "child_detached", { childWorkflowId: task.child_workflow_id });
|
|
296
|
+
db.prepare("UPDATE workflow_tasks SET child_workflow_id=NULL, result=NULL WHERE id=?").run(task.id);
|
|
297
|
+
}
|
|
298
|
+
db.prepare("DELETE FROM workflow_collect_checkpoint WHERE task_id=?").run(task.id);
|
|
299
|
+
const retryStatus: TaskStatus = task.type === "collect" ? "awaiting_user" : "running";
|
|
300
|
+
const workflowStatus: WorkflowStatus = task.type === "collect" ? "awaiting_user" : "running";
|
|
301
|
+
db.prepare("UPDATE workflow_tasks SET status = ?, attempts = attempts + 1, evaluation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(retryStatus, params.reasoning, task.id);
|
|
302
|
+
db.prepare("UPDATE workflows SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?").run(workflowStatus, item.id);
|
|
303
|
+
event(item.id, task.id, "rejected", { reasoning: params.reasoning });
|
|
304
|
+
return { content: [{ type: "text", text: task.type === "collect" ? `Critério reprovado. Permaneça na task #${task.position}.` : `Critério reprovado. Permaneça na task #${task.position} e repita a ação.` }], details: { accepted: false, task: taskSummary(currentTask(item.id)!) } };
|
|
305
|
+
} });
|
|
306
|
+
pi.registerTool({ name: "sequential_workflow_cancel", label: "Cancel Sequential Workflow", description: "Cancela um workflow ativo deste profile, sem cancelar seus descendentes.", promptSnippet: "Cancel a Sequential Workflow", promptGuidelines: ["Use sequential_workflow_cancel only after the user explicitly asks to cancel a named Sequential Workflow.", "sequential_workflow_cancel can cancel a workflow created in another session of the same profile; do not require adoption first.", "Cancelling a child workflow lets its parent handle the terminal child state; do not cancel descendants automatically."], parameters: Type.Object({ workflowId: Type.Integer({ minimum: 1 }) }), async execute(_id, params): Promise<{
|
|
307
|
+
content: {
|
|
308
|
+
type: "text";
|
|
309
|
+
text: string;
|
|
310
|
+
}[];
|
|
311
|
+
details: Record<string, unknown>;
|
|
312
|
+
}> { return cancelWorkflow(params.workflowId); } });
|
|
313
|
+
pi.registerTool({ name: "sequential_workflow_status", label: "Sequential Workflow Status", description: "Lista workflows ativos ou consulta o estado persistido de um workflow.", promptGuidelines: ["Use sequential_workflow_status only when the user explicitly asks to inspect, list, or continue a persisted Sequential Workflow. Never select or resume an existing workflow merely because it is active."], parameters: Type.Object({ workflowId: Type.Optional(Type.Integer({ minimum: 1 })), includeChildren: Type.Optional(Type.Boolean()) }), async execute(_id, params): Promise<{
|
|
314
|
+
content: {
|
|
315
|
+
type: "text";
|
|
316
|
+
text: string;
|
|
317
|
+
}[];
|
|
318
|
+
details: Record<string, unknown>;
|
|
319
|
+
}> {
|
|
320
|
+
if (params.workflowId === undefined) {
|
|
321
|
+
const items = many<Workflow>("SELECT id, title, source, status, parent_workflow_id FROM workflows WHERE session_id = ? AND status IN ('running', 'awaiting_user', 'evaluating') ORDER BY id DESC", harness.session());
|
|
322
|
+
return { content: [{ type: "text", text: items.length ? items.map((item) => `#${item.id} ${item.title} (${item.status})${item.parent_workflow_id ? `, pai #${item.parent_workflow_id}` : ""}; task #${currentTask(item.id)?.position ?? "nenhuma"}.`).join("\n") : "Não há workflows ativos." }], details: { workflows: items.map((item) => ({ ...item, currentTask: currentTask(item.id) })) } };
|
|
323
|
+
}
|
|
324
|
+
const item = workflow(params.workflowId);
|
|
325
|
+
if (!item)
|
|
326
|
+
throw new Error(`Workflow #${params.workflowId} não existe.`);
|
|
327
|
+
const tasks = many<Task>("SELECT id, workflow_id, position, type, instruction, criteria, status, attempts, result, evaluation, child_workflow_id FROM workflow_tasks WHERE workflow_id = ? ORDER BY position", item.id);
|
|
328
|
+
const children = params.includeChildren ? many<Workflow>("SELECT id, title, source, status, parent_workflow_id FROM workflows WHERE parent_workflow_id = ? ORDER BY id", item.id) : [];
|
|
329
|
+
return { content: [{ type: "text", text: `Workflow #${item.id} (${item.status}). Task atual: ${currentTask(item.id)?.position ?? "nenhuma"}.` }], details: { workflow: item, tasks, children } };
|
|
330
|
+
} });
|
|
331
|
+
pi.registerCommand("workflow-status", { description: "Show this session's workflows", handler: async (args, ctx) => {
|
|
332
|
+
try {
|
|
333
|
+
await harness.transaction(ctx, () => {
|
|
334
|
+
const rows = args.trim() ? [harness.own(Number(args.trim()))] : many<Workflow>("SELECT * FROM workflows WHERE session_id=? ORDER BY id DESC LIMIT 50", harness.session());
|
|
335
|
+
ctx.ui.notify(rows.map(w => `#${w.id} ${w.title}: ${w.status}`).join("\n") || "No workflows in this session.", "info");
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
ctx.ui.notify(String(error), "error");
|
|
340
|
+
}
|
|
341
|
+
} });
|
|
342
|
+
pi.registerCommand("workflow-cancel", { description: "Cancel an active workflow by ID without cancelling descendants", handler: async (args, ctx) => {
|
|
343
|
+
try {
|
|
344
|
+
await harness.transaction(ctx, () => {
|
|
345
|
+
cancelWorkflow(Number(args.trim()));
|
|
346
|
+
});
|
|
347
|
+
ctx.ui.notify("Workflow cancelled.", "warning");
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
ctx.ui.notify(String(error), "error");
|
|
351
|
+
}
|
|
352
|
+
} });
|
|
353
|
+
pi.on("session_shutdown", () => db.close());
|
|
270
354
|
}
|