loopengine 0.1.95 → 0.1.98
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 +21 -2
- package/dist/bin/ability-manager.d.ts +4 -0
- package/dist/bin/ability-manager.js +34 -2
- package/dist/bin/cli.d.ts +1 -1
- package/dist/bin/cli.js +3 -0
- package/dist/core/ability-details.d.ts +31 -0
- package/dist/core/ability-details.js +140 -0
- package/dist/core/ability-sources.d.ts +59 -0
- package/dist/core/ability-sources.js +312 -0
- package/dist/core/agent-config.d.ts +21 -1
- package/dist/core/discover-agents.js +11 -1
- package/dist/core/http-notifier.d.ts +7 -1
- package/dist/core/http-notifier.js +68 -9
- package/dist/core/http-notify-triggers/email.d.ts +2 -2
- package/dist/core/http-notify-triggers/email.js +2 -4
- package/dist/core/http-notify-triggers/lark.d.ts +2 -2
- package/dist/core/http-notify-triggers/lark.js +2 -4
- package/dist/core/http-notify-triggers/slack.d.ts +2 -2
- package/dist/core/http-notify-triggers/slack.js +2 -4
- package/dist/core/http-notify-triggers/telegram.d.ts +28 -0
- package/dist/core/http-notify-triggers/telegram.js +71 -0
- package/dist/core/http-notify-triggers/webhook.d.ts +2 -2
- package/dist/core/http-notify-triggers/webhook.js +2 -4
- package/dist/core/model-calls/meta-model-call.d.ts +15 -0
- package/dist/core/model-calls/meta-model-call.js +48 -0
- package/dist/core/model-calls/openai-model-call.d.ts +5 -0
- package/dist/core/model-calls/openai-model-call.js +47 -1
- package/dist/core/model-calls/xai-model-call.d.ts +15 -0
- package/dist/core/model-calls/xai-model-call.js +45 -0
- package/dist/core/notifier-file.d.ts +40 -6
- package/dist/core/notifier-file.js +119 -25
- package/dist/core/system-agents/workflow-planner.d.ts +100 -0
- package/dist/core/system-agents/workflow-planner.js +275 -0
- package/dist/core/workflows.d.ts +26 -0
- package/dist/core/workflows.js +39 -0
- package/dist/core/workspace-abilities.d.ts +31 -0
- package/dist/core/workspace-abilities.js +110 -0
- package/dist/index.d.ts +18 -4
- package/dist/index.js +17 -3
- package/dist/web/ability-details-admin.d.ts +17 -0
- package/dist/web/ability-details-admin.js +45 -0
- package/dist/web/ability-sources-admin.d.ts +33 -0
- package/dist/web/ability-sources-admin.js +65 -0
- package/dist/web/agent-file-admin.d.ts +1 -1
- package/dist/web/agent-file-admin.js +3 -0
- package/dist/web/agents-config-page.js +600 -220
- package/dist/web/agents-list-page.js +49 -20
- package/dist/web/console-shell.js +244 -3
- package/dist/web/dev-ui-styles.js +12 -0
- package/dist/web/global-config-page.js +348 -95
- package/dist/web/global-config.d.ts +44 -1
- package/dist/web/global-config.js +162 -2
- package/dist/web/interview-admin.js +2 -2
- package/dist/web/messaging-icons.d.ts +2 -0
- package/dist/web/messaging-icons.js +7 -0
- package/dist/web/notifier-admin.d.ts +33 -15
- package/dist/web/notifier-admin.js +109 -65
- package/dist/web/provider-icons.d.ts +2 -0
- package/dist/web/provider-icons.js +10 -0
- package/dist/web/templates-admin.d.ts +3 -0
- package/dist/web/templates-admin.js +5 -1
- package/dist/web/workflow-admin.d.ts +124 -0
- package/dist/web/workflow-admin.js +308 -0
- package/dist/web/workflow-page.d.ts +1 -0
- package/dist/web/workflow-page.js +226 -0
- package/dist/web/workspace-abilities-admin.d.ts +24 -0
- package/dist/web/workspace-abilities-admin.js +118 -0
- package/package.json +13 -1
|
@@ -110,7 +110,11 @@ export function templateChecklist(template, name, deps, failed = []) {
|
|
|
110
110
|
export async function createFromTemplate(id, deps) {
|
|
111
111
|
if (!('scaffoldAgent' in deps))
|
|
112
112
|
return createFromTemplateLegacy(id, deps);
|
|
113
|
-
|
|
113
|
+
return createFromTemplateObject(templateAt(id), deps);
|
|
114
|
+
}
|
|
115
|
+
/** createFromTemplate for a template that isn't one of the built-in ones:
|
|
116
|
+
* a workflow planned from a description (web/workflow-admin.ts). */
|
|
117
|
+
export async function createFromTemplateObject(template, deps) {
|
|
114
118
|
const { candidate, setup } = template;
|
|
115
119
|
const name = freeAgentName(candidate.agent.name, deps.agentExists);
|
|
116
120
|
// 1. The agent, and its permissions: everything asks unless a rule says
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type CreatedTemplateAgent, type TemplateChecklistItem, type TemplateCreateDeps } from './templates-admin.js';
|
|
2
|
+
import { type WorkflowPlan, type WorkflowPlannerTools } from '../core/system-agents/workflow-planner.js';
|
|
3
|
+
import { type WorkflowRecord } from '../core/workflows.js';
|
|
4
|
+
import type { ModelCall } from '../core/run-agent.js';
|
|
5
|
+
export declare class WorkflowError extends Error {
|
|
6
|
+
}
|
|
7
|
+
/** What the planner and the checks need to know about an agent. */
|
|
8
|
+
export interface WorkflowAgentInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
tools: string[];
|
|
11
|
+
abilities: string[];
|
|
12
|
+
rules: {
|
|
13
|
+
tool: string;
|
|
14
|
+
decision: 'allow' | 'ask' | 'deny';
|
|
15
|
+
scope: string;
|
|
16
|
+
}[];
|
|
17
|
+
defaultDecision: 'allow' | 'ask' | 'deny';
|
|
18
|
+
/** Its primary messaging app, if any. */
|
|
19
|
+
messaging: string | null;
|
|
20
|
+
/** Tools from its connected services (gateway-tools.yml), by source. */
|
|
21
|
+
connected?: {
|
|
22
|
+
source: string;
|
|
23
|
+
tools: string[];
|
|
24
|
+
}[];
|
|
25
|
+
}
|
|
26
|
+
/** A rule the workflow wants that the agent already decides differently. */
|
|
27
|
+
export interface WorkflowConflict {
|
|
28
|
+
tool: string;
|
|
29
|
+
has: 'allow' | 'ask' | 'deny';
|
|
30
|
+
wants: 'allow' | 'ask' | 'deny';
|
|
31
|
+
}
|
|
32
|
+
export interface WorkflowCheck {
|
|
33
|
+
/** For an existing agent: rules it already decides differently. */
|
|
34
|
+
conflicts: WorkflowConflict[];
|
|
35
|
+
/** Abilities the agent doesn't have yet (they'd be installed). */
|
|
36
|
+
toInstall: string[];
|
|
37
|
+
/** Connected-service tools it doesn't have yet (they'd be copied in). */
|
|
38
|
+
toConnect: string[];
|
|
39
|
+
/** Whether adding to this agent changes nothing it already does. */
|
|
40
|
+
fits: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** Checks a plan against the agent it would be added to. */
|
|
43
|
+
export declare function checkWorkflowForAgent(plan: WorkflowPlan, agent: WorkflowAgentInfo): WorkflowCheck;
|
|
44
|
+
/** Marks each step whose tool comes from some agent's connected service
|
|
45
|
+
* (whatever the model said) and drops that service from the abilities to
|
|
46
|
+
* install: it's copied over, not installed. */
|
|
47
|
+
export declare function resolveConnections(plan: WorkflowPlan, agents: WorkflowAgentInfo[]): void;
|
|
48
|
+
export interface PlannedWorkflow {
|
|
49
|
+
plan: WorkflowPlan;
|
|
50
|
+
/** The check against the suggested agent, when the plan names one. */
|
|
51
|
+
check?: WorkflowCheck;
|
|
52
|
+
/** Why the suggestion was changed, if it was (e.g. a conflict). */
|
|
53
|
+
note?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Plans a workflow from a description, then checks the suggested agent:
|
|
56
|
+
* one that would have a decision changed, or doesn't exist, becomes a new
|
|
57
|
+
* agent instead, saying why. */
|
|
58
|
+
export declare function planWorkflow(description: unknown, modelCall: ModelCall, tools: WorkflowPlannerTools & {
|
|
59
|
+
agentInfo(): Promise<WorkflowAgentInfo[]>;
|
|
60
|
+
}): Promise<PlannedWorkflow>;
|
|
61
|
+
export interface PlanJob {
|
|
62
|
+
id: string;
|
|
63
|
+
status: 'planning' | 'done' | 'failed';
|
|
64
|
+
startedAt: string;
|
|
65
|
+
result?: PlannedWorkflow;
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
/** Starts planning in the background; check on it with getPlanJob. */
|
|
69
|
+
export declare function startPlanJob(run: () => Promise<PlannedWorkflow>): PlanJob;
|
|
70
|
+
export declare function getPlanJob(id: string): PlanJob | undefined;
|
|
71
|
+
export interface CreatedWorkflow {
|
|
72
|
+
agent: string;
|
|
73
|
+
/** A new agent was made for it. */
|
|
74
|
+
newAgent: boolean;
|
|
75
|
+
workflow: string;
|
|
76
|
+
abilities: {
|
|
77
|
+
installed: string[];
|
|
78
|
+
failed: {
|
|
79
|
+
name: string;
|
|
80
|
+
error: string;
|
|
81
|
+
}[];
|
|
82
|
+
};
|
|
83
|
+
/** Rules left as the agent already had them. */
|
|
84
|
+
keptDecisions: WorkflowConflict[];
|
|
85
|
+
checklist: TemplateChecklistItem[];
|
|
86
|
+
schedule?: CreatedTemplateAgent['schedule'];
|
|
87
|
+
scheduleError?: string;
|
|
88
|
+
loadError?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface WorkflowCreateDeps extends TemplateCreateDeps {
|
|
91
|
+
agentDir(name: string): string;
|
|
92
|
+
agentInfo(): Promise<WorkflowAgentInfo[]>;
|
|
93
|
+
/** Makes sure the agent is loaded once its new tools and skill are in place. */
|
|
94
|
+
reloadAgent(name: string): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
/** Creates the workflow: in a new agent (as a template would), or in an
|
|
97
|
+
* existing one. An existing agent keeps every decision it already has: a
|
|
98
|
+
* conflicting rule is left alone and reported, not changed. */
|
|
99
|
+
export declare function createWorkflow(input: {
|
|
100
|
+
plan?: unknown;
|
|
101
|
+
target?: unknown;
|
|
102
|
+
description?: unknown;
|
|
103
|
+
}, deps: WorkflowCreateDeps): Promise<CreatedWorkflow>;
|
|
104
|
+
export interface AgentWorkflowView extends WorkflowRecord {
|
|
105
|
+
schedule?: {
|
|
106
|
+
taskId: string;
|
|
107
|
+
status: string;
|
|
108
|
+
nextRunAt?: string;
|
|
109
|
+
lastRunAt?: string;
|
|
110
|
+
lastStatus?: string;
|
|
111
|
+
waiting: boolean;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** An agent's workflows, each with its schedule's state when it has one. */
|
|
115
|
+
export declare function listAgentWorkflows(agentName: string, agentDir: string): Promise<AgentWorkflowView[]>;
|
|
116
|
+
/** Starts a workflow now: its schedule runs once (a paused one goes back
|
|
117
|
+
* to paused after), or, with no schedule, a one-off run. Questions and
|
|
118
|
+
* approvals go to the agent's messaging app. */
|
|
119
|
+
export declare function runWorkflowNow(agentName: string, agentDir: string, id: string): Promise<{
|
|
120
|
+
taskId: string;
|
|
121
|
+
}>;
|
|
122
|
+
/** Forgets a workflow (its skill, schedule and rules stay). */
|
|
123
|
+
export declare function forgetWorkflow(agentDir: string, id: string): void;
|
|
124
|
+
export declare function agentDirIn(projectDir: string, name: string): string;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// Backs "Describe a workflow" (web/workflow-page.ts) and the Workflows list
|
|
2
|
+
// on an agent's Overview: plans a workflow from a description
|
|
3
|
+
// (core/system-agents/workflow-planner.ts), checks the plan against the
|
|
4
|
+
// agent it would go into, and creates it: as a new agent through the same
|
|
5
|
+
// path as a template (web/templates-admin.ts), or added to an existing
|
|
6
|
+
// one without changing anything that agent already does.
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { addGatewayTool, readGatewayToolsFromDir } from '../core/gateway-tools.js';
|
|
9
|
+
import { addActauthRule, readActauthConfig } from './actauth-admin.js';
|
|
10
|
+
import { writeSkill } from './skills-admin.js';
|
|
11
|
+
import { createFromTemplateObject, installFailureReason, templateChecklist } from './templates-admin.js';
|
|
12
|
+
import { templateScheduleStart } from '../core/system-agents/templates.js';
|
|
13
|
+
import { planWorkflowWithModel, slugify, workflowTemplate } from '../core/system-agents/workflow-planner.js';
|
|
14
|
+
import { readWorkflows, removeWorkflow, saveWorkflow } from '../core/workflows.js';
|
|
15
|
+
import { createScheduledTask, listScheduledTasks, pauseScheduledTask, runScheduledTaskNow } from '../core/scheduler.js';
|
|
16
|
+
import { workspaceTimeZone } from '../core/time-zone.js';
|
|
17
|
+
export class WorkflowError extends Error {
|
|
18
|
+
}
|
|
19
|
+
/** Checks a plan against the agent it would be added to. */
|
|
20
|
+
export function checkWorkflowForAgent(plan, agent) {
|
|
21
|
+
const conflicts = plan.rules.flatMap((rule) => {
|
|
22
|
+
const existing = agent.rules.find((r) => r.tool === rule.tool && r.scope === '*/*') ?? agent.rules.find((r) => r.tool === rule.tool);
|
|
23
|
+
return existing && existing.decision !== rule.decision ? [{ tool: rule.tool, has: existing.decision, wants: rule.decision }] : [];
|
|
24
|
+
});
|
|
25
|
+
const toInstall = plan.abilities.filter((spec) => !agent.abilities.some((name) => specNames(spec).includes(name)));
|
|
26
|
+
const toConnect = [...new Set(plan.steps.flatMap((s) => (s.connection && s.tool && !agent.tools.includes(s.tool) ? [s.tool] : [])))];
|
|
27
|
+
return { conflicts, toInstall, toConnect, fits: conflicts.length === 0 };
|
|
28
|
+
}
|
|
29
|
+
/** Marks each step whose tool comes from some agent's connected service
|
|
30
|
+
* (whatever the model said) and drops that service from the abilities to
|
|
31
|
+
* install: it's copied over, not installed. */
|
|
32
|
+
export function resolveConnections(plan, agents) {
|
|
33
|
+
const sources = new Set();
|
|
34
|
+
for (const step of plan.steps) {
|
|
35
|
+
if (!step.tool)
|
|
36
|
+
continue;
|
|
37
|
+
const found = agents.flatMap((a) => a.connected ?? []).find((c) => c.tools.includes(step.tool));
|
|
38
|
+
if (!found)
|
|
39
|
+
continue;
|
|
40
|
+
step.connection = found.source;
|
|
41
|
+
if (step.ability === found.source)
|
|
42
|
+
delete step.ability;
|
|
43
|
+
sources.add(found.source);
|
|
44
|
+
}
|
|
45
|
+
plan.abilities = plan.abilities.filter((spec) => !sources.has(spec) || plan.steps.some((s) => s.ability === spec));
|
|
46
|
+
}
|
|
47
|
+
/** Copies the connected-service tools the workflow uses into `target`,
|
|
48
|
+
* from an agent that has them. Returns the tools copied. */
|
|
49
|
+
function copyConnectedTools(plan, target, agents, agentDir) {
|
|
50
|
+
const has = new Set(agents.find((a) => a.name === target)?.tools ?? []);
|
|
51
|
+
const copied = [];
|
|
52
|
+
const failed = [];
|
|
53
|
+
for (const step of plan.steps) {
|
|
54
|
+
const tool = step.tool;
|
|
55
|
+
if (!step.connection || !tool || has.has(tool) || copied.includes(tool))
|
|
56
|
+
continue;
|
|
57
|
+
const from = agents.find((a) => a.connected?.some((c) => c.source === step.connection && c.tools.includes(tool)));
|
|
58
|
+
const entry = from && readGatewayToolsFromDir(agentDir(from.name)).find((e) => e.name === step.connection);
|
|
59
|
+
const slug = tool.slice(String(step.connection).length + 1);
|
|
60
|
+
if (!entry || !entry.slugs.includes(slug)) {
|
|
61
|
+
failed.push({ name: tool, error: `no agent has ${tool} connected any more` });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const resolved = entry.resolved?.[slug];
|
|
66
|
+
addGatewayTool(target, { provider: entry.provider, name: entry.name, slugs: [slug], ...(entry.cliCommand ? { cliCommand: entry.cliCommand } : {}), ...(resolved ? { resolved: { [slug]: resolved } } : {}) });
|
|
67
|
+
copied.push(tool);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
failed.push({ name: tool, error: err instanceof Error ? err.message : String(err) });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { copied, failed };
|
|
74
|
+
}
|
|
75
|
+
/** The package names a spec could install as: "lp-x", "@acme/lp-x@1.0.0" →
|
|
76
|
+
* "@acme/lp-x", "file:/…/lp-x" or "github:org/lp-x" → "lp-x". */
|
|
77
|
+
function specNames(spec) {
|
|
78
|
+
const bare = spec.replace(/^file:/, '').replace(/^github:/, '').replace(/#.*$/, '');
|
|
79
|
+
const last = bare.split('/').pop() ?? bare;
|
|
80
|
+
const scoped = spec.match(/^(@[^/]+\/[^@]+)/)?.[1];
|
|
81
|
+
const named = spec.match(/^([^@/:][^@]*)(@.*)?$/)?.[1];
|
|
82
|
+
return [...new Set([scoped, named, last.replace(/@.*$/, '')].filter((n) => Boolean(n)))];
|
|
83
|
+
}
|
|
84
|
+
/** Plans a workflow from a description, then checks the suggested agent:
|
|
85
|
+
* one that would have a decision changed, or doesn't exist, becomes a new
|
|
86
|
+
* agent instead, saying why. */
|
|
87
|
+
export async function planWorkflow(description, modelCall, tools) {
|
|
88
|
+
const text = typeof description === 'string' ? description.trim() : '';
|
|
89
|
+
if (text.length < 15)
|
|
90
|
+
throw new WorkflowError('Describe the workflow in a sentence or two: what starts it, the steps, and where you want to decide.');
|
|
91
|
+
if (text.length > 4000)
|
|
92
|
+
throw new WorkflowError('That description is too long: keep it under 4000 characters.');
|
|
93
|
+
const result = await planWorkflowWithModel(modelCall, text, tools);
|
|
94
|
+
if (!result.plan)
|
|
95
|
+
throw new WorkflowError(result.reply || 'The planner did not produce a plan; try describing the steps more plainly.');
|
|
96
|
+
const plan = result.plan;
|
|
97
|
+
const agents = await tools.agentInfo();
|
|
98
|
+
resolveConnections(plan, agents);
|
|
99
|
+
if (plan.target.kind !== 'existing' || !plan.target.agent)
|
|
100
|
+
return { plan };
|
|
101
|
+
const agent = agents.find((a) => a.name === plan.target.agent);
|
|
102
|
+
if (!agent) {
|
|
103
|
+
plan.target = { kind: 'new', reason: `${plan.target.agent} doesn't exist, so it would be a new agent.` };
|
|
104
|
+
return { plan, note: plan.target.reason };
|
|
105
|
+
}
|
|
106
|
+
const check = checkWorkflowForAgent(plan, agent);
|
|
107
|
+
if (!check.fits) {
|
|
108
|
+
const note = `Adding it to ${agent.name} would change ${check.conflicts.map((c) => `${c.tool} (${c.has} there, ${c.wants} here)`).join(', ')}, so a new agent is suggested.`;
|
|
109
|
+
plan.target = { kind: 'new', reason: note };
|
|
110
|
+
return { plan, check, note };
|
|
111
|
+
}
|
|
112
|
+
return { plan, check };
|
|
113
|
+
}
|
|
114
|
+
const JOB_TTL_MS = 60 * 60 * 1000;
|
|
115
|
+
const planJobs = new Map();
|
|
116
|
+
/** Starts planning in the background; check on it with getPlanJob. */
|
|
117
|
+
export function startPlanJob(run) {
|
|
118
|
+
for (const [id, job] of planJobs)
|
|
119
|
+
if (Date.now() - job.at > JOB_TTL_MS)
|
|
120
|
+
planJobs.delete(id);
|
|
121
|
+
const id = `plan-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
122
|
+
const job = { id, status: 'planning', startedAt: new Date().toISOString(), at: Date.now() };
|
|
123
|
+
planJobs.set(id, job);
|
|
124
|
+
run().then((result) => planJobs.set(id, { ...job, status: 'done', result }), (err) => planJobs.set(id, { ...job, status: 'failed', error: err instanceof Error ? err.message : String(err) }));
|
|
125
|
+
const { at: _at, ...view } = job;
|
|
126
|
+
return view;
|
|
127
|
+
}
|
|
128
|
+
export function getPlanJob(id) {
|
|
129
|
+
const job = planJobs.get(id);
|
|
130
|
+
if (!job)
|
|
131
|
+
return undefined;
|
|
132
|
+
const { at: _at, ...view } = job;
|
|
133
|
+
return view;
|
|
134
|
+
}
|
|
135
|
+
/** A to-do for each step nothing could be found for, first in the list. */
|
|
136
|
+
function withMissingSteps(plan, agent, checklist) {
|
|
137
|
+
const href = `/agents/config?agent=${encodeURIComponent(agent)}&tab=abilities`;
|
|
138
|
+
const missing = plan.steps.flatMap((s) => (s.missing ? [{ label: `Find or add a tool for "${s.title}": ${s.missing}`, href }] : []));
|
|
139
|
+
return [...missing, ...checklist];
|
|
140
|
+
}
|
|
141
|
+
/** Creates the workflow: in a new agent (as a template would), or in an
|
|
142
|
+
* existing one. An existing agent keeps every decision it already has: a
|
|
143
|
+
* conflicting rule is left alone and reported, not changed. */
|
|
144
|
+
export async function createWorkflow(input, deps) {
|
|
145
|
+
const plan = input.plan;
|
|
146
|
+
if (!plan || typeof plan !== 'object' || !Array.isArray(plan.steps) || !plan.skill || !plan.newAgent)
|
|
147
|
+
throw new WorkflowError('Plan the workflow first.');
|
|
148
|
+
const target = (input.target ?? plan.target);
|
|
149
|
+
const description = typeof input.description === 'string' ? input.description.trim().slice(0, 4000) : undefined;
|
|
150
|
+
const record = (scheduleName) => ({
|
|
151
|
+
id: plan.id,
|
|
152
|
+
name: plan.name,
|
|
153
|
+
summary: plan.summary,
|
|
154
|
+
trigger: { kind: plan.trigger.kind, description: plan.trigger.description },
|
|
155
|
+
steps: plan.steps,
|
|
156
|
+
startMessage: plan.startMessage,
|
|
157
|
+
...(scheduleName ? { scheduleName } : {}),
|
|
158
|
+
...(description ? { description } : {}),
|
|
159
|
+
createdAt: new Date().toISOString(),
|
|
160
|
+
});
|
|
161
|
+
const agents = await deps.agentInfo();
|
|
162
|
+
resolveConnections(plan, agents);
|
|
163
|
+
const template = workflowTemplate(plan);
|
|
164
|
+
if (target.kind !== 'existing') {
|
|
165
|
+
const created = await createFromTemplateObject(template, deps);
|
|
166
|
+
// Read on its next run: the agent needs no reloading for it.
|
|
167
|
+
const connected = copyConnectedTools(plan, created.name, agents, deps.agentDir);
|
|
168
|
+
const loadError = created.loadError;
|
|
169
|
+
saveWorkflow(deps.agentDir(created.name), record(created.schedule ? plan.name : undefined));
|
|
170
|
+
return {
|
|
171
|
+
agent: created.name,
|
|
172
|
+
newAgent: true,
|
|
173
|
+
workflow: plan.name,
|
|
174
|
+
abilities: { installed: [...created.abilities.installed, ...connected.copied], failed: [...created.abilities.failed, ...connected.failed] },
|
|
175
|
+
keptDecisions: [],
|
|
176
|
+
checklist: withMissingSteps(plan, created.name, created.checklist),
|
|
177
|
+
...(created.schedule ? { schedule: created.schedule } : {}),
|
|
178
|
+
...(created.scheduleError ? { scheduleError: created.scheduleError } : {}),
|
|
179
|
+
...(loadError ? { loadError } : {}),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const agent = agents.find((a) => a.name === target.agent);
|
|
183
|
+
if (!agent)
|
|
184
|
+
throw new WorkflowError(`There's no agent named ${target.agent ?? '(none)'}.`);
|
|
185
|
+
const check = checkWorkflowForAgent(plan, agent);
|
|
186
|
+
// 1. Rules: only for tools the agent has no rule for.
|
|
187
|
+
const { rules: current } = readActauthConfig(agent.name);
|
|
188
|
+
for (const rule of plan.rules) {
|
|
189
|
+
if (current.some((r) => r.tool === rule.tool))
|
|
190
|
+
continue;
|
|
191
|
+
let name = slugify(`${plan.id}-${rule.tool}`, `${plan.id}-rule`);
|
|
192
|
+
for (let i = 2; current.some((r) => r.name === name); i++)
|
|
193
|
+
name = `${slugify(`${plan.id}-${rule.tool}`)}-${i}`;
|
|
194
|
+
addActauthRule(agent.name, { name, scope: '*/*', tool: rule.tool, decision: rule.decision });
|
|
195
|
+
current.push({ name, scope: '*/*', tool: rule.tool, decision: rule.decision });
|
|
196
|
+
}
|
|
197
|
+
// 2. Abilities it doesn't have yet.
|
|
198
|
+
const installed = [];
|
|
199
|
+
const failed = [];
|
|
200
|
+
for (const spec of check.toInstall) {
|
|
201
|
+
if (!deps.installAbility) {
|
|
202
|
+
failed.push({ name: spec, error: 'installing abilities needs LOOPENGINE_ADMIN_AUTH set on the server' });
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await deps.installAbility(agent.name, spec);
|
|
207
|
+
installed.push(spec);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
211
|
+
if (/already installed/i.test(message))
|
|
212
|
+
continue;
|
|
213
|
+
failed.push({ name: spec, error: installFailureReason(err) });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Connected-service tools it doesn't have yet.
|
|
217
|
+
const connected = copyConnectedTools(plan, agent.name, agents, deps.agentDir);
|
|
218
|
+
installed.push(...connected.copied);
|
|
219
|
+
failed.push(...connected.failed);
|
|
220
|
+
// 3. Its steps, as a skill; then load the agent again with all of it.
|
|
221
|
+
writeSkill(agent.name, plan.id, { description: plan.skill.description, body: plan.skill.body });
|
|
222
|
+
let loadError;
|
|
223
|
+
try {
|
|
224
|
+
await deps.reloadAgent(agent.name);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
loadError = err instanceof Error ? err.message : String(err);
|
|
228
|
+
}
|
|
229
|
+
// 4. Its schedule, paused until the person turns it on.
|
|
230
|
+
let schedule;
|
|
231
|
+
let scheduleError;
|
|
232
|
+
if (template.setup.schedule) {
|
|
233
|
+
try {
|
|
234
|
+
const startAt = templateScheduleStart(template.setup.schedule);
|
|
235
|
+
const task = await createScheduledTask({
|
|
236
|
+
agent: agent.name,
|
|
237
|
+
message: plan.startMessage,
|
|
238
|
+
schedule: { every: template.setup.schedule.every, ...(startAt ? { start_at: startAt } : {}) },
|
|
239
|
+
task_name: plan.name,
|
|
240
|
+
});
|
|
241
|
+
await pauseScheduledTask(task.task_id);
|
|
242
|
+
schedule = { description: plan.trigger.description, timeZone: workspaceTimeZone(), ...(startAt ? { next_run_at: startAt } : {}) };
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
scheduleError = err instanceof Error ? err.message : String(err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
saveWorkflow(deps.agentDir(agent.name), record(schedule ? plan.name : undefined));
|
|
249
|
+
return {
|
|
250
|
+
agent: agent.name,
|
|
251
|
+
newAgent: false,
|
|
252
|
+
workflow: plan.name,
|
|
253
|
+
abilities: { installed, failed },
|
|
254
|
+
keptDecisions: check.conflicts,
|
|
255
|
+
checklist: withMissingSteps(plan, agent.name, templateChecklist(template, agent.name, deps, failed)),
|
|
256
|
+
...(schedule ? { schedule } : {}),
|
|
257
|
+
...(scheduleError ? { scheduleError } : {}),
|
|
258
|
+
...(loadError ? { loadError } : {}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** An agent's workflows, each with its schedule's state when it has one. */
|
|
262
|
+
export async function listAgentWorkflows(agentName, agentDir) {
|
|
263
|
+
const workflows = readWorkflows(agentDir);
|
|
264
|
+
if (!workflows.length)
|
|
265
|
+
return [];
|
|
266
|
+
const tasks = await listScheduledTasks({ agent: agentName });
|
|
267
|
+
return workflows.map((w) => {
|
|
268
|
+
const task = w.scheduleName ? tasks.find((t) => t.task_name === w.scheduleName && t.status !== 'cancelled') : undefined;
|
|
269
|
+
return task
|
|
270
|
+
? {
|
|
271
|
+
...w,
|
|
272
|
+
schedule: {
|
|
273
|
+
taskId: task.task_id,
|
|
274
|
+
status: task.status,
|
|
275
|
+
...(task.next_run_at ? { nextRunAt: task.next_run_at } : {}),
|
|
276
|
+
...(task.last_run_at ? { lastRunAt: task.last_run_at } : {}),
|
|
277
|
+
...(task.last_status ? { lastStatus: task.last_status } : {}),
|
|
278
|
+
waiting: Boolean(task.pending) || task.status === 'awaiting_approval',
|
|
279
|
+
},
|
|
280
|
+
}
|
|
281
|
+
: w;
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/** Starts a workflow now: its schedule runs once (a paused one goes back
|
|
285
|
+
* to paused after), or, with no schedule, a one-off run. Questions and
|
|
286
|
+
* approvals go to the agent's messaging app. */
|
|
287
|
+
export async function runWorkflowNow(agentName, agentDir, id) {
|
|
288
|
+
const workflow = readWorkflows(agentDir).find((w) => w.id === id);
|
|
289
|
+
if (!workflow)
|
|
290
|
+
throw new WorkflowError(`No workflow ${id} in ${agentName}.`);
|
|
291
|
+
const views = await listAgentWorkflows(agentName, agentDir);
|
|
292
|
+
const task = views.find((w) => w.id === id)?.schedule;
|
|
293
|
+
if (task) {
|
|
294
|
+
if (task.status === 'running' || task.waiting)
|
|
295
|
+
throw new WorkflowError('It is already running, or waiting for a decision.');
|
|
296
|
+
await runScheduledTaskNow(task.taskId);
|
|
297
|
+
return { taskId: task.taskId };
|
|
298
|
+
}
|
|
299
|
+
const once = await createScheduledTask({ agent: agentName, message: workflow.startMessage, schedule: { run_at: new Date().toISOString() }, task_name: `${workflow.name} (run now)` });
|
|
300
|
+
return { taskId: once.task_id };
|
|
301
|
+
}
|
|
302
|
+
/** Forgets a workflow (its skill, schedule and rules stay). */
|
|
303
|
+
export function forgetWorkflow(agentDir, id) {
|
|
304
|
+
removeWorkflow(agentDir, id);
|
|
305
|
+
}
|
|
306
|
+
export function agentDirIn(projectDir, name) {
|
|
307
|
+
return join(projectDir, 'agents', name);
|
|
308
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const workflowPageHtml: string;
|