create-loopengine 0.1.77 → 0.1.78
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/package.json +1 -1
- package/template/adapters/http.ts +193 -0
- package/template/package.json +1 -1
package/package.json
CHANGED
|
@@ -195,6 +195,16 @@ import {
|
|
|
195
195
|
removeAbilitySourceFromConsole,
|
|
196
196
|
saveAbilitySourceFromConsole,
|
|
197
197
|
abilityDetails,
|
|
198
|
+
WorkflowError,
|
|
199
|
+
createWorkflow,
|
|
200
|
+
forgetWorkflow,
|
|
201
|
+
getPlanJob,
|
|
202
|
+
listAgentWorkflows,
|
|
203
|
+
planWorkflow,
|
|
204
|
+
runWorkflowNow,
|
|
205
|
+
startPlanJob,
|
|
206
|
+
type WorkflowAgentInfo,
|
|
207
|
+
workflowPageHtml,
|
|
198
208
|
MAX_UPLOAD_BYTES,
|
|
199
209
|
installUploadedAbility,
|
|
200
210
|
installWorkspaceAbility,
|
|
@@ -1014,6 +1024,175 @@ async function handleInterviews(req: IncomingMessage, res: ServerResponse, pathn
|
|
|
1014
1024
|
}
|
|
1015
1025
|
}
|
|
1016
1026
|
|
|
1027
|
+
// "Describe a workflow" (loopengine's web/workflow-admin.ts): what the planner and the
|
|
1028
|
+
// checks know about each agent: its tools, abilities, rules and primary
|
|
1029
|
+
// messaging app.
|
|
1030
|
+
async function workflowAgentInfo(): Promise<WorkflowAgentInfo[]> {
|
|
1031
|
+
return Promise.all(
|
|
1032
|
+
listAgents().flatMap((name) => {
|
|
1033
|
+
const entry = getEntry(name)
|
|
1034
|
+
if (!entry) return []
|
|
1035
|
+
return [(async (): Promise<WorkflowAgentInfo> => {
|
|
1036
|
+
const { rules, defaultDecision } = readActauthConfig(name)
|
|
1037
|
+
// Abilities by default: gateway tools (Composio and the like) are
|
|
1038
|
+
// left out, so a workflow is never built on one.
|
|
1039
|
+
let tools: string[] = []
|
|
1040
|
+
let gateway = new Set<string>()
|
|
1041
|
+
try {
|
|
1042
|
+
gateway = new Set((await loadGatewayToolsFromDir(agentDir(name))).map((t) => t.name))
|
|
1043
|
+
const own = entry.config.tools ?? (await loadDefaultTools(entry.config))
|
|
1044
|
+
const all = [...own, ...(await loadSubagentAsTools(entry.config))]
|
|
1045
|
+
tools = [...new Set(all.map((t) => t.name))].filter((t) => !gateway.has(t))
|
|
1046
|
+
} catch {}
|
|
1047
|
+
let abilities: string[] = []
|
|
1048
|
+
try {
|
|
1049
|
+
abilities = listInstalledAbilities(name).map((a) => a.name)
|
|
1050
|
+
} catch {}
|
|
1051
|
+
let messaging: string | null = null
|
|
1052
|
+
try {
|
|
1053
|
+
const n = describeAgentNotifier(entry.config, defaultWebhook)
|
|
1054
|
+
messaging = n.source === 'none' ? null : (n.primary ?? n.channel ?? null)
|
|
1055
|
+
} catch {}
|
|
1056
|
+
const decided = (d: unknown): d is 'allow' | 'ask' | 'deny' => d === 'allow' || d === 'ask' || d === 'deny'
|
|
1057
|
+
return {
|
|
1058
|
+
name,
|
|
1059
|
+
tools,
|
|
1060
|
+
abilities,
|
|
1061
|
+
rules: rules.flatMap((r) => (decided(r.decision) && !gateway.has(r.tool) ? [{ tool: r.tool, decision: r.decision, scope: r.scope }] : [])),
|
|
1062
|
+
defaultDecision: decided(defaultDecision) ? defaultDecision : 'ask',
|
|
1063
|
+
messaging,
|
|
1064
|
+
}
|
|
1065
|
+
})()]
|
|
1066
|
+
}),
|
|
1067
|
+
)
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function handleWorkflows(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void> {
|
|
1071
|
+
const json = (status: number, body: unknown): void => {
|
|
1072
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
|
|
1073
|
+
}
|
|
1074
|
+
const [action, jobId] = pathname.split('/').slice(2) // after /workflows
|
|
1075
|
+
const dirOf = (name: string) => join(projectDir(), 'agents', name)
|
|
1076
|
+
try {
|
|
1077
|
+
// No model to plan with until an agent names one and its key is set
|
|
1078
|
+
// (building the model call throws without the key).
|
|
1079
|
+
let picked: ReturnType<typeof pickInterviewModel>
|
|
1080
|
+
try {
|
|
1081
|
+
picked = pickInterviewModel(interviewAgents())
|
|
1082
|
+
} catch {
|
|
1083
|
+
picked = undefined
|
|
1084
|
+
}
|
|
1085
|
+
if (!action && req.method === 'GET') return json(200, { ready: Boolean(picked), canInstallAbilities: secretsProtected, agents: listAgents() })
|
|
1086
|
+
// A planning job's progress (the page checks every few seconds).
|
|
1087
|
+
if (action === 'plan' && jobId && req.method === 'GET') {
|
|
1088
|
+
const job = getPlanJob(jobId)
|
|
1089
|
+
return job ? json(200, job) : json(404, { error: 'No such planning job (it may have expired); plan it again.' })
|
|
1090
|
+
}
|
|
1091
|
+
if (action === 'plan' && !jobId && req.method === 'POST') {
|
|
1092
|
+
if (!picked) return json(409, { error: 'No model is ready to plan with: set one on an agent (Overview → Model) and add its key under Models.' })
|
|
1093
|
+
const body = await readJsonBody(req)
|
|
1094
|
+
const base = interviewTools(interviewAgents())
|
|
1095
|
+
// The same model, with room for a whole plan in one reply (an
|
|
1096
|
+
// agent's own model call stops at its maxTokens, often 4096).
|
|
1097
|
+
let modelCall = picked.modelCall
|
|
1098
|
+
try {
|
|
1099
|
+
modelCall = (await synthesizeCreateModelCall({ ...picked.model, maxTokens: 8192 } as AgentModelConfig))()
|
|
1100
|
+
} catch {}
|
|
1101
|
+
if (typeof body.description !== 'string' || body.description.trim().length < 15) {
|
|
1102
|
+
return json(400, { error: 'Describe the workflow in a sentence or two: what starts it, the steps, and where you want to decide.' })
|
|
1103
|
+
}
|
|
1104
|
+
const job = startPlanJob(() => planWorkflow(body.description, modelCall, {
|
|
1105
|
+
agentInfo: workflowAgentInfo,
|
|
1106
|
+
// A summary: an agent's first tools and its rules that aren't the
|
|
1107
|
+
// default, to keep what the model reads short.
|
|
1108
|
+
listAgents: async () => (await workflowAgentInfo()).map((a) => ({
|
|
1109
|
+
name: a.name,
|
|
1110
|
+
abilities: a.abilities,
|
|
1111
|
+
messaging: a.messaging,
|
|
1112
|
+
defaultDecision: a.defaultDecision,
|
|
1113
|
+
toolCount: a.tools.length,
|
|
1114
|
+
tools: a.tools.slice(0, 40),
|
|
1115
|
+
rules: a.rules.filter((r) => r.decision !== a.defaultDecision).slice(0, 40).map((r) => `${r.tool}: ${r.decision}`),
|
|
1116
|
+
})),
|
|
1117
|
+
searchPublicAbilities: (query) => base.searchPublicAbilities(query),
|
|
1118
|
+
listWorkspaceAbilities: async () => listWorkspaceAbilities().map((a) => ({ name: a.name, description: a.description, spec: a.spec })),
|
|
1119
|
+
abilityDetails: async (spec) => {
|
|
1120
|
+
const d = await abilityDetails(spec)
|
|
1121
|
+
return { name: d.name, tools: d.tools.map((t) => ({ name: t.name, description: t.description.slice(0, 300), decision: t.decision ?? null })), settings: d.settings.map((x) => x.name) }
|
|
1122
|
+
},
|
|
1123
|
+
listConnections: () => base.listConnections(),
|
|
1124
|
+
}))
|
|
1125
|
+
return json(202, job)
|
|
1126
|
+
}
|
|
1127
|
+
if (action === 'create' && req.method === 'POST') {
|
|
1128
|
+
const body = await readJsonBody(req)
|
|
1129
|
+
const model = pickAgentModel(interviewAgents())
|
|
1130
|
+
const created = await createWorkflow(body, {
|
|
1131
|
+
agentExists: (name) => Boolean(getEntry(name)) || existsSync(dirOf(name)),
|
|
1132
|
+
agentDir: dirOf,
|
|
1133
|
+
agentInfo: workflowAgentInfo,
|
|
1134
|
+
scaffoldAgent: async (name, systemPrompt) => {
|
|
1135
|
+
const parsed = parseAgentTemplateOptions({ systemPrompt, ...(model ? { model } : {}) })
|
|
1136
|
+
if (!parsed.ok) throw new WorkflowError(parsed.error)
|
|
1137
|
+
await scaffoldAgent(projectDir(), name, parsed.value)
|
|
1138
|
+
},
|
|
1139
|
+
registerAgent: async (name) => {
|
|
1140
|
+
const indexPath = join(dirOf(name), 'index.ts')
|
|
1141
|
+
registerAgent(await loadAgentModule(indexPath, indexPath))
|
|
1142
|
+
},
|
|
1143
|
+
// Skills, rules and gateway tools are read on every run, so a
|
|
1144
|
+
// loaded agent already sees them; new ability code restarts the
|
|
1145
|
+
// server (exitIfCodeChanged). Only an agent not loaded yet is.
|
|
1146
|
+
reloadAgent: async (name) => {
|
|
1147
|
+
if (getEntry(name)) return
|
|
1148
|
+
const indexPath = join(dirOf(name), 'index.ts')
|
|
1149
|
+
registerAgent(await loadAgentModule(indexPath, indexPath))
|
|
1150
|
+
},
|
|
1151
|
+
// As for templates and the Abilities tab: installing runs
|
|
1152
|
+
// third-party code, so only behind the admin login.
|
|
1153
|
+
...(secretsProtected ? { installAbility: async (name: string, spec: string) => void (await installAbility(name, spec)) } : {}),
|
|
1154
|
+
envValue: (name, variable) => createAgentEnv(name, dirOf(name)).get(variable),
|
|
1155
|
+
messagingConfigured: (name) => {
|
|
1156
|
+
const entry = getEntry(name)
|
|
1157
|
+
return entry ? describeAgentNotifier(entry.config, defaultWebhook).source !== 'none' : false
|
|
1158
|
+
},
|
|
1159
|
+
})
|
|
1160
|
+
json(200, created)
|
|
1161
|
+
// New tools in an agent that was already running load with a restart,
|
|
1162
|
+
// as after installing an ability from its tab.
|
|
1163
|
+
if (!created.newAgent && created.abilities.installed.length) exitIfCodeChanged(res)
|
|
1164
|
+
return
|
|
1165
|
+
}
|
|
1166
|
+
return json(404, { error: 'not found' })
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1169
|
+
if (err instanceof WorkflowError) return json(400, { error: message })
|
|
1170
|
+
console.error('[loopengine] workflow failed:', err)
|
|
1171
|
+
return json(502, { error: `Couldn't plan or create the workflow: ${message}` })
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// An agent's workflows (its Overview): list, run now, forget.
|
|
1176
|
+
async function handleAgentWorkflows(req: IncomingMessage, res: ServerResponse, agentName: string, id: string | undefined, action: string | undefined): Promise<void> {
|
|
1177
|
+
const json = (status: number, body: unknown): void => {
|
|
1178
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
|
|
1179
|
+
}
|
|
1180
|
+
if (!getEntry(agentName)) return json(404, { error: `unknown agent '${agentName}'` })
|
|
1181
|
+
const dir = agentDir(agentName)
|
|
1182
|
+
try {
|
|
1183
|
+
if (!id && req.method === 'GET') return json(200, { workflows: await listAgentWorkflows(agentName, dir) })
|
|
1184
|
+
if (id && action === 'run' && req.method === 'POST') return json(200, await runWorkflowNow(agentName, dir, id))
|
|
1185
|
+
if (id && !action && req.method === 'DELETE') {
|
|
1186
|
+
forgetWorkflow(dir, id)
|
|
1187
|
+
return json(200, { workflows: await listAgentWorkflows(agentName, dir) })
|
|
1188
|
+
}
|
|
1189
|
+
return json(404, { error: 'not found' })
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
1192
|
+
return json(err instanceof WorkflowError ? 400 : 500, { error: message })
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1017
1196
|
// Templates (loopengine's web/templates-admin.ts): ready-made agents,
|
|
1018
1197
|
// created through the same scaffold and registration as Agents → + New.
|
|
1019
1198
|
// A template agent takes the model of an agent that already works here,
|
|
@@ -3488,6 +3667,20 @@ const server = createServer(async (req, res) => {
|
|
|
3488
3667
|
await handleInterviews(req, res, pathname)
|
|
3489
3668
|
return
|
|
3490
3669
|
}
|
|
3670
|
+
// "Describe a workflow": the page, and its routes.
|
|
3671
|
+
if (req.method === 'GET' && pathname === '/workflows/new') {
|
|
3672
|
+
res.writeHead(200, { 'content-type': 'text/html' }).end(workflowPageHtml)
|
|
3673
|
+
return
|
|
3674
|
+
}
|
|
3675
|
+
if (pathname === '/workflows' || pathname === '/workflows/plan' || pathname.startsWith('/workflows/plan/') || pathname === '/workflows/create') {
|
|
3676
|
+
await handleWorkflows(req, res, pathname)
|
|
3677
|
+
return
|
|
3678
|
+
}
|
|
3679
|
+
const agentWorkflowsMatch = pathname.match(/^\/agents\/([^/]+)\/workflows(?:\/([a-z0-9-]+))?(?:\/(run))?$/)
|
|
3680
|
+
if (agentWorkflowsMatch) {
|
|
3681
|
+
await handleAgentWorkflows(req, res, decodeURIComponent(agentWorkflowsMatch[1]), agentWorkflowsMatch[2], agentWorkflowsMatch[3])
|
|
3682
|
+
return
|
|
3683
|
+
}
|
|
3491
3684
|
// Templates: the page, and its routes.
|
|
3492
3685
|
if (req.method === 'GET' && pathname === '/templates') {
|
|
3493
3686
|
res.writeHead(200, { 'content-type': 'text/html' }).end(templatesPageHtml)
|