create-loopengine 0.1.50 → 0.1.52
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/_gitignore +1 -0
- package/template/adapters/http.ts +135 -5
- package/template/package.json +1 -1
package/package.json
CHANGED
package/template/_gitignore
CHANGED
|
@@ -100,6 +100,12 @@ import {
|
|
|
100
100
|
agentDirFor,
|
|
101
101
|
verifyProxyIdentity,
|
|
102
102
|
TRUSTED_PROXY_IDENTITY_HEADER,
|
|
103
|
+
NotifierFileError,
|
|
104
|
+
describeAgentNotifier,
|
|
105
|
+
listAgentPending,
|
|
106
|
+
removeAgentNotifier,
|
|
107
|
+
saveAgentNotifier,
|
|
108
|
+
testAgentNotifier,
|
|
103
109
|
EnvVarNameError,
|
|
104
110
|
readActauthConfig,
|
|
105
111
|
addActauthRule,
|
|
@@ -178,6 +184,11 @@ const defaultHttpWebhookNotifier =
|
|
|
178
184
|
defaultHttpWebhookUrl && defaultHttpWebhookSecret
|
|
179
185
|
? new WebhookNotifier({ webhookUrl: defaultHttpWebhookUrl, signingSecret: defaultHttpWebhookSecret })
|
|
180
186
|
: undefined
|
|
187
|
+
// The same default, as plain settings, for the console's Approvals
|
|
188
|
+
// section (web/notifier-admin.ts) to show and test.
|
|
189
|
+
const defaultWebhook =
|
|
190
|
+
defaultHttpWebhookUrl && defaultHttpWebhookSecret ? { webhookUrl: defaultHttpWebhookUrl, webhookSecret: defaultHttpWebhookSecret } : undefined
|
|
191
|
+
|
|
181
192
|
|
|
182
193
|
// Used when an AgentConfig doesn't define its own sessionIdFor — a plain
|
|
183
194
|
// client-supplied key, same shape adapters/cli.ts's --session flag
|
|
@@ -382,6 +393,10 @@ async function describeAgent(entry: RegistryEntry): Promise<Record<string, unkno
|
|
|
382
393
|
contextBudgetTokens: config.contextBudgetTokens ?? 100000,
|
|
383
394
|
skillIndexBudgetTokens: config.skillIndexBudgetTokens ?? 2000,
|
|
384
395
|
skillsDirs: skillsDirs,
|
|
396
|
+
// How many abilities are installed — just the count, for the console's
|
|
397
|
+
// tab badge; the Abilities tab's own GET .../abilities has the detail
|
|
398
|
+
// (and its slower npm version lookups).
|
|
399
|
+
abilityCount: listInstalledAbilities(config.name).length,
|
|
385
400
|
skills: skillIndex.map((s) => ({ name: s.name, description: s.description })),
|
|
386
401
|
systemSkills: systemSkillIndex.map((s) => ({ name: s.name, description: s.description })),
|
|
387
402
|
tools: tools.map(describeTool),
|
|
@@ -1030,6 +1045,76 @@ function handleEnvGet(res: ServerResponse, agentName: string): void {
|
|
|
1030
1045
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(listDeclaredEnvVars(agentName)))
|
|
1031
1046
|
}
|
|
1032
1047
|
|
|
1048
|
+
// Backs the console's Permissions → Approvals section — see
|
|
1049
|
+
// web/notifier-admin.ts. Reading is open like the rest of the admin
|
|
1050
|
+
// surface; saving or removing writes a secret and a file, so it gets the
|
|
1051
|
+
// same refuse-without-auth gate as the env routes below.
|
|
1052
|
+
function handleNotifierGet(res: ServerResponse, agentName: string): void {
|
|
1053
|
+
const entry = getEntry(agentName)
|
|
1054
|
+
if (!entry) {
|
|
1055
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1056
|
+
return
|
|
1057
|
+
}
|
|
1058
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(describeAgentNotifier(entry.config, defaultWebhook)))
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async function handleNotifierPut(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
|
|
1062
|
+
if (!secretsProtected) {
|
|
1063
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(
|
|
1064
|
+
JSON.stringify({ error: 'Refusing to save a notifier without LOOPENGINE_ADMIN_AUTH configured — set it first, since this stores a secret.' }),
|
|
1065
|
+
)
|
|
1066
|
+
return
|
|
1067
|
+
}
|
|
1068
|
+
const entry = getEntry(agentName)
|
|
1069
|
+
if (!entry) {
|
|
1070
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1071
|
+
return
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
await saveAgentNotifier(entry.config, await readJsonBody(req))
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
const status = err instanceof NotifierFileError ? 400 : 500
|
|
1077
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
1078
|
+
return
|
|
1079
|
+
}
|
|
1080
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(describeAgentNotifier(entry.config, defaultWebhook)))
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function handleNotifierDelete(res: ServerResponse, agentName: string): void {
|
|
1084
|
+
if (!secretsProtected) {
|
|
1085
|
+
res.writeHead(403, { 'content-type': 'application/json' }).end(
|
|
1086
|
+
JSON.stringify({ error: 'Refusing to remove a notifier without LOOPENGINE_ADMIN_AUTH configured.' }),
|
|
1087
|
+
)
|
|
1088
|
+
return
|
|
1089
|
+
}
|
|
1090
|
+
const entry = getEntry(agentName)
|
|
1091
|
+
if (!entry) {
|
|
1092
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1093
|
+
return
|
|
1094
|
+
}
|
|
1095
|
+
removeAgentNotifier(entry.config)
|
|
1096
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(describeAgentNotifier(entry.config, defaultWebhook)))
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
async function handleNotifierTest(res: ServerResponse, agentName: string): Promise<void> {
|
|
1100
|
+
const entry = getEntry(agentName)
|
|
1101
|
+
if (!entry) {
|
|
1102
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1103
|
+
return
|
|
1104
|
+
}
|
|
1105
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(await testAgentNotifier(entry.config, defaultWebhook)))
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Every approval/question still waiting for this agent — for an app that
|
|
1109
|
+
// can't receive webhooks (it polls this instead) and for the console.
|
|
1110
|
+
async function handlePendingGet(res: ServerResponse, agentName: string): Promise<void> {
|
|
1111
|
+
if (!getEntry(agentName)) {
|
|
1112
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
1113
|
+
return
|
|
1114
|
+
}
|
|
1115
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ pending: await listAgentPending(checkpoints, agentName) }))
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1033
1118
|
// `?slot=agent` targets this agent's own agents/<name>/.env (a per-agent
|
|
1034
1119
|
// value, read by its tools through ToolContext.env) instead of the
|
|
1035
1120
|
// project-wide .env — see loopengine's DeclaredEnvVar.slot.
|
|
@@ -1481,7 +1566,25 @@ async function handleSessionGet(req: IncomingMessage, res: ServerResponse, agent
|
|
|
1481
1566
|
}
|
|
1482
1567
|
|
|
1483
1568
|
type PendingSignal = { type: 'question'; entry: PendingQuestion } | { type: 'approval'; entry: PendingApproval }
|
|
1484
|
-
type FinishedTurn = { text: string; stopReason?: 'max_turns' | 'denied' | 'pending_approval' | 'pending_question' }
|
|
1569
|
+
type FinishedTurn = { text: string; stopReason?: 'max_turns' | 'denied' | 'pending_approval' | 'pending_question'; pending?: PendingSummary[] }
|
|
1570
|
+
|
|
1571
|
+
// What a caller needs to act on a turn that paused for a durable
|
|
1572
|
+
// decision: which decision, and where to send it — the same pendingId and
|
|
1573
|
+
// path the notifier's webhook carries, so an app can resolve straight
|
|
1574
|
+
// from this response instead of waiting for the webhook or polling.
|
|
1575
|
+
type PendingSummary = { pendingId: string; kind: 'approval' | 'question'; tool: string; args: Record<string, unknown>; reason: string; resolvePath?: string; answerPath?: string }
|
|
1576
|
+
|
|
1577
|
+
function pendingSummary(pending: RunAgentResult['pending']): PendingSummary[] | undefined {
|
|
1578
|
+
if (!pending?.outstanding.length) return undefined
|
|
1579
|
+
return pending.outstanding.map((item) => ({
|
|
1580
|
+
pendingId: item.pendingId,
|
|
1581
|
+
kind: item.kind,
|
|
1582
|
+
tool: item.tool,
|
|
1583
|
+
args: item.args,
|
|
1584
|
+
reason: item.reason,
|
|
1585
|
+
...(item.kind === 'question' ? { answerPath: `/pending-questions/${item.pendingId}/answer` } : { resolvePath: `/pending-approvals/${item.pendingId}/resolve` }),
|
|
1586
|
+
}))
|
|
1587
|
+
}
|
|
1485
1588
|
|
|
1486
1589
|
// One entry per turn currently in flight for the plain (non-streaming)
|
|
1487
1590
|
// /messages route, keyed by "<agentName>:<rawSessionId>" — the same
|
|
@@ -1617,6 +1720,7 @@ async function raceAndRespond(res: ServerResponse, agentName: string, rawSession
|
|
|
1617
1720
|
// final-text summary of it.
|
|
1618
1721
|
const responseBody: Record<string, unknown> = { text: winner.turnResult.text, sessionId: rawSessionId, events: state.events }
|
|
1619
1722
|
if (winner.turnResult.stopReason) responseBody.stopReason = winner.turnResult.stopReason
|
|
1723
|
+
if (winner.turnResult.pending) responseBody.pending = winner.turnResult.pending
|
|
1620
1724
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(responseBody))
|
|
1621
1725
|
}
|
|
1622
1726
|
|
|
@@ -1695,7 +1799,7 @@ async function handleMessages(req: IncomingMessage, res: ServerResponse, agentNa
|
|
|
1695
1799
|
// raceAndRespond) always ends the same way regardless of which route
|
|
1696
1800
|
// produced it.
|
|
1697
1801
|
state.events.push({ type: 'done', text: result.text, ...(result.stopReason ? { stopReason: result.stopReason } : {}) })
|
|
1698
|
-
return { newMessages: result.newMessages, result: { text: result.text, stopReason: result.stopReason } }
|
|
1802
|
+
return { newMessages: result.newMessages, result: { text: result.text, stopReason: result.stopReason, pending: pendingSummary(result.pending) } }
|
|
1699
1803
|
})
|
|
1700
1804
|
state.turnPromise = turnPromise
|
|
1701
1805
|
|
|
@@ -1801,7 +1905,7 @@ async function handleMessagesStream(req: IncomingMessage, res: ServerResponse, a
|
|
|
1801
1905
|
const doneEvent: LoopEvent = { type: 'done', text: result.text, ...(result.stopReason ? { stopReason: result.stopReason } : {}) }
|
|
1802
1906
|
writeSseEvent(res, doneEvent)
|
|
1803
1907
|
state.events.push(doneEvent)
|
|
1804
|
-
return { newMessages: result.newMessages, result: { text: result.text, stopReason: result.stopReason } }
|
|
1908
|
+
return { newMessages: result.newMessages, result: { text: result.text, stopReason: result.stopReason, pending: pendingSummary(result.pending) } }
|
|
1805
1909
|
})
|
|
1806
1910
|
state.turnPromise = turnPromise
|
|
1807
1911
|
|
|
@@ -2143,11 +2247,11 @@ async function respondAfterResolution(res: ServerResponse, finalCheckpoint: Turn
|
|
|
2143
2247
|
if (result.pending) {
|
|
2144
2248
|
await createCheckpointFromPending(finalCheckpoint.agent, finalCheckpoint.tenant, finalCheckpoint.sessionId, result.pending)
|
|
2145
2249
|
}
|
|
2146
|
-
return { newMessages: result.newMessages, result: { text: result.text, stopReason: result.stopReason } }
|
|
2250
|
+
return { newMessages: result.newMessages, result: { text: result.text, stopReason: result.stopReason, pending: pendingSummary(result.pending) } }
|
|
2147
2251
|
})
|
|
2148
2252
|
|
|
2149
2253
|
res.writeHead(200, { 'content-type': 'application/json' }).end(
|
|
2150
|
-
JSON.stringify({ text: responseBody.text, sessionId: finalCheckpoint.sessionId, ...(responseBody.stopReason ? { stopReason: responseBody.stopReason } : {}) }),
|
|
2254
|
+
JSON.stringify({ text: responseBody.text, sessionId: finalCheckpoint.sessionId, ...(responseBody.stopReason ? { stopReason: responseBody.stopReason } : {}), ...(responseBody.pending ? { pending: responseBody.pending } : {}) }),
|
|
2151
2255
|
)
|
|
2152
2256
|
}
|
|
2153
2257
|
|
|
@@ -2709,6 +2813,32 @@ const server = createServer(async (req, res) => {
|
|
|
2709
2813
|
return
|
|
2710
2814
|
}
|
|
2711
2815
|
|
|
2816
|
+
// Backs the console's Approvals section and polling clients — see
|
|
2817
|
+
// handleNotifierGet/handlePendingGet above.
|
|
2818
|
+
const notifierMatch = pathname.match(/^\/agents\/([^/]+)\/notifier$/)
|
|
2819
|
+
if (notifierMatch && req.method === 'GET') {
|
|
2820
|
+
handleNotifierGet(res, decodeURIComponent(notifierMatch[1]))
|
|
2821
|
+
return
|
|
2822
|
+
}
|
|
2823
|
+
if (notifierMatch && req.method === 'PUT') {
|
|
2824
|
+
await handleNotifierPut(req, res, decodeURIComponent(notifierMatch[1]))
|
|
2825
|
+
return
|
|
2826
|
+
}
|
|
2827
|
+
if (notifierMatch && req.method === 'DELETE') {
|
|
2828
|
+
handleNotifierDelete(res, decodeURIComponent(notifierMatch[1]))
|
|
2829
|
+
return
|
|
2830
|
+
}
|
|
2831
|
+
const notifierTestMatch = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/notifier\/test$/)
|
|
2832
|
+
if (notifierTestMatch) {
|
|
2833
|
+
await handleNotifierTest(res, decodeURIComponent(notifierTestMatch[1]))
|
|
2834
|
+
return
|
|
2835
|
+
}
|
|
2836
|
+
const pendingListMatch = req.method === 'GET' && pathname.match(/^\/agents\/([^/]+)\/pending$/)
|
|
2837
|
+
if (pendingListMatch) {
|
|
2838
|
+
await handlePendingGet(res, decodeURIComponent(pendingListMatch[1]))
|
|
2839
|
+
return
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2712
2842
|
// Backs the Admin UI's "Environment" section — see
|
|
2713
2843
|
// handleEnvGet/handleEnvPut's own doc comments above.
|
|
2714
2844
|
const envListMatch = pathname.match(/^\/agents\/([^/]+)\/env$/)
|