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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.51",
3
+ "version": "0.1.52",
4
4
  "description": "Scaffold a new loopengine agent project — a runnable HTTP server with one example agent, in your own repo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,7 @@
1
1
  node_modules
2
2
  dist
3
3
  .sessions
4
+ .checkpoints
4
5
  .env
5
6
  *.log
6
7
  .DS_Store
@@ -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$/)
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.63",
12
+ "loopengine": "^0.1.67",
13
13
  "actauth": "^0.0.12"
14
14
  },
15
15
  "devDependencies": {