create-loopengine 0.1.72 → 0.1.73

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.72",
3
+ "version": "0.1.73",
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",
@@ -4,6 +4,8 @@ dist
4
4
  .checkpoints
5
5
  # Slack threads matched to their requests (agents/<name>/.messaging).
6
6
  .messaging
7
+ # "Find your first agent" interviews (the console page /interview).
8
+ .interviews
7
9
  .scheduler
8
10
  .env
9
11
  *.log
@@ -72,6 +72,20 @@ import {
72
72
  playgroundHtml,
73
73
  agentsConfigPageHtml,
74
74
  agentsListPageHtml,
75
+ interviewPageHtml,
76
+ InterviewError,
77
+ answerInterview,
78
+ createFromCandidate,
79
+ deleteInterview,
80
+ finishInterview,
81
+ getInterview,
82
+ interviewTools,
83
+ listInterviews,
84
+ pickInterviewModel,
85
+ previewCandidate,
86
+ startInterview,
87
+ setInterviewProject,
88
+ type InterviewAgentSource,
75
89
  globalConfigPageHtml,
76
90
  addGatewayTool,
77
91
  agentDir,
@@ -879,6 +893,71 @@ function parseAgentLimitsFields(
879
893
  return { ok: true, value }
880
894
  }
881
895
 
896
+ // "Find your first agent" (web/interview-admin.ts): the interview's
897
+ // routes, behind the console login like the rest. The interviewer runs on
898
+ // the model of an agent that already works here; creating an agent goes
899
+ // through the same scaffold and registration as Agents → + New.
900
+ function interviewAgents(): InterviewAgentSource[] {
901
+ return listAgents().flatMap((name) => {
902
+ const entry = getEntry(name)
903
+ return entry ? [{ name, config: entry.config, createModelCall: () => entry.createModelCall() }] : []
904
+ })
905
+ }
906
+
907
+ async function handleInterviews(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void> {
908
+ setInterviewProject(projectDir())
909
+ const json = (status: number, body: unknown): void => {
910
+ res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
911
+ }
912
+ const parts = pathname.split('/').slice(2).map(decodeURIComponent) // after /interviews
913
+ const [id, action, index, sub] = parts
914
+ try {
915
+ if (!id) {
916
+ if (req.method === 'GET') return json(200, { interviews: listInterviews(), ready: Boolean(pickInterviewModel(interviewAgents())) })
917
+ if (req.method === 'POST') return json(200, startInterview())
918
+ return json(405, { error: 'method not allowed' })
919
+ }
920
+ if (!action) {
921
+ if (req.method === 'GET') return json(200, getInterview(id))
922
+ if (req.method === 'DELETE') {
923
+ deleteInterview(id)
924
+ return json(200, { deleted: true })
925
+ }
926
+ return json(405, { error: 'method not allowed' })
927
+ }
928
+ const agents = interviewAgents()
929
+ const exists = (name: string) => Boolean(getEntry(name)) || existsSync(join(projectDir(), 'agents', name))
930
+ if (action === 'candidates' && index !== undefined && sub === 'preview' && req.method === 'GET') {
931
+ return json(200, previewCandidate(id, Number(index), { agentExists: exists }))
932
+ }
933
+ const picked = pickInterviewModel(agents)
934
+ if (!picked) return json(409, { error: 'No agent here names a model yet. Set one on an agent (Overview → Model) and add its key under Models.' })
935
+ if (action === 'messages' && req.method === 'POST') {
936
+ const body = await readJsonBody(req)
937
+ return json(200, await answerInterview(id, body.text, picked.modelCall, interviewTools(agents)))
938
+ }
939
+ if (action === 'finish' && req.method === 'POST') return json(200, await finishInterview(id, picked.modelCall, interviewTools(agents)))
940
+ if (action === 'candidates' && index !== undefined && sub === 'create' && req.method === 'POST') {
941
+ const created = await createFromCandidate(id, Number(index), {
942
+ agentExists: exists,
943
+ createAgent: async (name, systemPrompt) => {
944
+ const parsed = parseAgentTemplateOptions({ systemPrompt, model: picked.model })
945
+ if (!parsed.ok) throw new InterviewError(parsed.error)
946
+ const indexPath = await scaffoldAgent(projectDir(), name, parsed.value)
947
+ registerAgent(await loadAgentModule(indexPath, indexPath))
948
+ },
949
+ })
950
+ return json(200, created)
951
+ }
952
+ return json(404, { error: 'not found' })
953
+ } catch (err) {
954
+ const message = err instanceof Error ? err.message : String(err)
955
+ if (err instanceof InterviewError) return json(message === 'No such interview' || message === 'No such candidate' ? 404 : 400, { error: message })
956
+ console.error('[loopengine] interview failed:', err)
957
+ return json(502, { error: `The interviewer couldn't answer: ${message}` })
958
+ }
959
+ }
960
+
882
961
  async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Promise<void> {
883
962
  const body = await readJsonBody(req)
884
963
  if (typeof body.name !== 'string' || !body.name) {
@@ -3184,6 +3263,15 @@ const server = createServer(async (req, res) => {
3184
3263
  await handleCreateAgent(req, res)
3185
3264
  return
3186
3265
  }
3266
+ // "Find your first agent": the page, and its routes.
3267
+ if (req.method === 'GET' && pathname === '/interview') {
3268
+ res.writeHead(200, { 'content-type': 'text/html' }).end(interviewPageHtml)
3269
+ return
3270
+ }
3271
+ if (pathname === '/interviews' || pathname.startsWith('/interviews/')) {
3272
+ await handleInterviews(req, res, pathname)
3273
+ return
3274
+ }
3187
3275
  const editAgentMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)$/)
3188
3276
  if (editAgentMatch) {
3189
3277
  await handleEditAgent(req, res, decodeURIComponent(editAgentMatch[1]))
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.1.89",
12
+ "loopengine": "^0.1.91",
13
13
  "actauth": "^0.0.12",
14
14
  "tsx": "^4.19.0",
15
15
  "typescript": "^5.7.0"