create-loopengine 0.1.51 → 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 +131 -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
|
|
@@ -1034,6 +1045,76 @@ function handleEnvGet(res: ServerResponse, agentName: string): void {
|
|
|
1034
1045
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(listDeclaredEnvVars(agentName)))
|
|
1035
1046
|
}
|
|
1036
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
|
+
|
|
1037
1118
|
// `?slot=agent` targets this agent's own agents/<name>/.env (a per-agent
|
|
1038
1119
|
// value, read by its tools through ToolContext.env) instead of the
|
|
1039
1120
|
// project-wide .env — see loopengine's DeclaredEnvVar.slot.
|
|
@@ -1485,7 +1566,25 @@ async function handleSessionGet(req: IncomingMessage, res: ServerResponse, agent
|
|
|
1485
1566
|
}
|
|
1486
1567
|
|
|
1487
1568
|
type PendingSignal = { type: 'question'; entry: PendingQuestion } | { type: 'approval'; entry: PendingApproval }
|
|
1488
|
-
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
|
+
}
|
|
1489
1588
|
|
|
1490
1589
|
// One entry per turn currently in flight for the plain (non-streaming)
|
|
1491
1590
|
// /messages route, keyed by "<agentName>:<rawSessionId>" — the same
|
|
@@ -1621,6 +1720,7 @@ async function raceAndRespond(res: ServerResponse, agentName: string, rawSession
|
|
|
1621
1720
|
// final-text summary of it.
|
|
1622
1721
|
const responseBody: Record<string, unknown> = { text: winner.turnResult.text, sessionId: rawSessionId, events: state.events }
|
|
1623
1722
|
if (winner.turnResult.stopReason) responseBody.stopReason = winner.turnResult.stopReason
|
|
1723
|
+
if (winner.turnResult.pending) responseBody.pending = winner.turnResult.pending
|
|
1624
1724
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(responseBody))
|
|
1625
1725
|
}
|
|
1626
1726
|
|
|
@@ -1699,7 +1799,7 @@ async function handleMessages(req: IncomingMessage, res: ServerResponse, agentNa
|
|
|
1699
1799
|
// raceAndRespond) always ends the same way regardless of which route
|
|
1700
1800
|
// produced it.
|
|
1701
1801
|
state.events.push({ type: 'done', text: result.text, ...(result.stopReason ? { stopReason: result.stopReason } : {}) })
|
|
1702
|
-
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) } }
|
|
1703
1803
|
})
|
|
1704
1804
|
state.turnPromise = turnPromise
|
|
1705
1805
|
|
|
@@ -1805,7 +1905,7 @@ async function handleMessagesStream(req: IncomingMessage, res: ServerResponse, a
|
|
|
1805
1905
|
const doneEvent: LoopEvent = { type: 'done', text: result.text, ...(result.stopReason ? { stopReason: result.stopReason } : {}) }
|
|
1806
1906
|
writeSseEvent(res, doneEvent)
|
|
1807
1907
|
state.events.push(doneEvent)
|
|
1808
|
-
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) } }
|
|
1809
1909
|
})
|
|
1810
1910
|
state.turnPromise = turnPromise
|
|
1811
1911
|
|
|
@@ -2147,11 +2247,11 @@ async function respondAfterResolution(res: ServerResponse, finalCheckpoint: Turn
|
|
|
2147
2247
|
if (result.pending) {
|
|
2148
2248
|
await createCheckpointFromPending(finalCheckpoint.agent, finalCheckpoint.tenant, finalCheckpoint.sessionId, result.pending)
|
|
2149
2249
|
}
|
|
2150
|
-
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) } }
|
|
2151
2251
|
})
|
|
2152
2252
|
|
|
2153
2253
|
res.writeHead(200, { 'content-type': 'application/json' }).end(
|
|
2154
|
-
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 } : {}) }),
|
|
2155
2255
|
)
|
|
2156
2256
|
}
|
|
2157
2257
|
|
|
@@ -2713,6 +2813,32 @@ const server = createServer(async (req, res) => {
|
|
|
2713
2813
|
return
|
|
2714
2814
|
}
|
|
2715
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
|
+
|
|
2716
2842
|
// Backs the Admin UI's "Environment" section — see
|
|
2717
2843
|
// handleEnvGet/handleEnvPut's own doc comments above.
|
|
2718
2844
|
const envListMatch = pathname.match(/^\/agents\/([^/]+)\/env$/)
|