create-loopengine 0.1.72 → 0.1.74
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 +2 -0
- package/template/adapters/http.ts +141 -0
- package/template/package.json +1 -1
package/package.json
CHANGED
package/template/_gitignore
CHANGED
|
@@ -72,6 +72,26 @@ 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
|
+
pickAgentModel,
|
|
86
|
+
previewCandidate,
|
|
87
|
+
startInterview,
|
|
88
|
+
setInterviewProject,
|
|
89
|
+
type InterviewAgentSource,
|
|
90
|
+
templatesPageHtml,
|
|
91
|
+
TemplateError,
|
|
92
|
+
createFromTemplate,
|
|
93
|
+
listTemplates,
|
|
94
|
+
previewTemplate,
|
|
75
95
|
globalConfigPageHtml,
|
|
76
96
|
addGatewayTool,
|
|
77
97
|
agentDir,
|
|
@@ -879,6 +899,109 @@ function parseAgentLimitsFields(
|
|
|
879
899
|
return { ok: true, value }
|
|
880
900
|
}
|
|
881
901
|
|
|
902
|
+
// "Find your first agent" (web/interview-admin.ts): the interview's
|
|
903
|
+
// routes, behind the console login like the rest. The interviewer runs on
|
|
904
|
+
// the model of an agent that already works here; creating an agent goes
|
|
905
|
+
// through the same scaffold and registration as Agents → + New.
|
|
906
|
+
function interviewAgents(): InterviewAgentSource[] {
|
|
907
|
+
return listAgents().flatMap((name) => {
|
|
908
|
+
const entry = getEntry(name)
|
|
909
|
+
return entry ? [{ name, config: entry.config, createModelCall: () => entry.createModelCall() }] : []
|
|
910
|
+
})
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function handleInterviews(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void> {
|
|
914
|
+
setInterviewProject(projectDir())
|
|
915
|
+
const json = (status: number, body: unknown): void => {
|
|
916
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
|
|
917
|
+
}
|
|
918
|
+
const parts = pathname.split('/').slice(2).map(decodeURIComponent) // after /interviews
|
|
919
|
+
const [id, action, index, sub] = parts
|
|
920
|
+
try {
|
|
921
|
+
if (!id) {
|
|
922
|
+
if (req.method === 'GET') return json(200, { interviews: listInterviews(), ready: Boolean(pickInterviewModel(interviewAgents())) })
|
|
923
|
+
if (req.method === 'POST') return json(200, startInterview())
|
|
924
|
+
return json(405, { error: 'method not allowed' })
|
|
925
|
+
}
|
|
926
|
+
if (!action) {
|
|
927
|
+
if (req.method === 'GET') return json(200, getInterview(id))
|
|
928
|
+
if (req.method === 'DELETE') {
|
|
929
|
+
deleteInterview(id)
|
|
930
|
+
return json(200, { deleted: true })
|
|
931
|
+
}
|
|
932
|
+
return json(405, { error: 'method not allowed' })
|
|
933
|
+
}
|
|
934
|
+
const agents = interviewAgents()
|
|
935
|
+
const exists = (name: string) => Boolean(getEntry(name)) || existsSync(join(projectDir(), 'agents', name))
|
|
936
|
+
if (action === 'candidates' && index !== undefined && sub === 'preview' && req.method === 'GET') {
|
|
937
|
+
return json(200, previewCandidate(id, Number(index), { agentExists: exists }))
|
|
938
|
+
}
|
|
939
|
+
const picked = pickInterviewModel(agents)
|
|
940
|
+
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.' })
|
|
941
|
+
if (action === 'messages' && req.method === 'POST') {
|
|
942
|
+
const body = await readJsonBody(req)
|
|
943
|
+
return json(200, await answerInterview(id, body.text, picked.modelCall, interviewTools(agents)))
|
|
944
|
+
}
|
|
945
|
+
if (action === 'finish' && req.method === 'POST') return json(200, await finishInterview(id, picked.modelCall, interviewTools(agents)))
|
|
946
|
+
if (action === 'candidates' && index !== undefined && sub === 'create' && req.method === 'POST') {
|
|
947
|
+
const created = await createFromCandidate(id, Number(index), {
|
|
948
|
+
agentExists: exists,
|
|
949
|
+
createAgent: async (name, systemPrompt) => {
|
|
950
|
+
const parsed = parseAgentTemplateOptions({ systemPrompt, model: picked.model })
|
|
951
|
+
if (!parsed.ok) throw new InterviewError(parsed.error)
|
|
952
|
+
const indexPath = await scaffoldAgent(projectDir(), name, parsed.value)
|
|
953
|
+
registerAgent(await loadAgentModule(indexPath, indexPath))
|
|
954
|
+
},
|
|
955
|
+
})
|
|
956
|
+
return json(200, created)
|
|
957
|
+
}
|
|
958
|
+
return json(404, { error: 'not found' })
|
|
959
|
+
} catch (err) {
|
|
960
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
961
|
+
if (err instanceof InterviewError) return json(message === 'No such interview' || message === 'No such candidate' ? 404 : 400, { error: message })
|
|
962
|
+
console.error('[loopengine] interview failed:', err)
|
|
963
|
+
return json(502, { error: `The interviewer couldn't answer: ${message}` })
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Templates (loopengine's web/templates-admin.ts): ready-made agents,
|
|
968
|
+
// created through the same scaffold and registration as Agents → + New.
|
|
969
|
+
// A template agent takes the model of an agent that already works here,
|
|
970
|
+
// as an interview's does; with none, it starts on the scaffold's default.
|
|
971
|
+
async function handleStarterTemplates(req: IncomingMessage, res: ServerResponse, pathname: string): Promise<void> {
|
|
972
|
+
const json = (status: number, body: unknown): void => {
|
|
973
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' }).end(JSON.stringify(body))
|
|
974
|
+
}
|
|
975
|
+
const [id, action] = pathname.split('/').slice(2).map(decodeURIComponent) // after /starter-templates
|
|
976
|
+
const exists = (name: string) => Boolean(getEntry(name)) || existsSync(join(projectDir(), 'agents', name))
|
|
977
|
+
try {
|
|
978
|
+
if (!id) {
|
|
979
|
+
if (req.method === 'GET') return json(200, { templates: listTemplates() })
|
|
980
|
+
return json(405, { error: 'method not allowed' })
|
|
981
|
+
}
|
|
982
|
+
if (action === 'preview' && req.method === 'GET') return json(200, previewTemplate(id, { agentExists: exists }))
|
|
983
|
+
if (action === 'create' && req.method === 'POST') {
|
|
984
|
+
const model = pickAgentModel(interviewAgents())
|
|
985
|
+
const created = await createFromTemplate(id, {
|
|
986
|
+
agentExists: exists,
|
|
987
|
+
createAgent: async (name, systemPrompt) => {
|
|
988
|
+
const parsed = parseAgentTemplateOptions({ systemPrompt, ...(model ? { model } : {}) })
|
|
989
|
+
if (!parsed.ok) throw new TemplateError(parsed.error)
|
|
990
|
+
const indexPath = await scaffoldAgent(projectDir(), name, parsed.value)
|
|
991
|
+
registerAgent(await loadAgentModule(indexPath, indexPath))
|
|
992
|
+
},
|
|
993
|
+
})
|
|
994
|
+
return json(200, created)
|
|
995
|
+
}
|
|
996
|
+
return json(404, { error: 'not found' })
|
|
997
|
+
} catch (err) {
|
|
998
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
999
|
+
if (err instanceof TemplateError) return json(message === 'No such template' ? 404 : 400, { error: message })
|
|
1000
|
+
console.error('[loopengine] template failed:', err)
|
|
1001
|
+
return json(500, { error: `Couldn't create it: ${message}` })
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
882
1005
|
async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
883
1006
|
const body = await readJsonBody(req)
|
|
884
1007
|
if (typeof body.name !== 'string' || !body.name) {
|
|
@@ -3184,6 +3307,24 @@ const server = createServer(async (req, res) => {
|
|
|
3184
3307
|
await handleCreateAgent(req, res)
|
|
3185
3308
|
return
|
|
3186
3309
|
}
|
|
3310
|
+
// "Find your first agent": the page, and its routes.
|
|
3311
|
+
if (req.method === 'GET' && pathname === '/interview') {
|
|
3312
|
+
res.writeHead(200, { 'content-type': 'text/html' }).end(interviewPageHtml)
|
|
3313
|
+
return
|
|
3314
|
+
}
|
|
3315
|
+
if (pathname === '/interviews' || pathname.startsWith('/interviews/')) {
|
|
3316
|
+
await handleInterviews(req, res, pathname)
|
|
3317
|
+
return
|
|
3318
|
+
}
|
|
3319
|
+
// Templates: the page, and its routes.
|
|
3320
|
+
if (req.method === 'GET' && pathname === '/templates') {
|
|
3321
|
+
res.writeHead(200, { 'content-type': 'text/html' }).end(templatesPageHtml)
|
|
3322
|
+
return
|
|
3323
|
+
}
|
|
3324
|
+
if (pathname === '/starter-templates' || pathname.startsWith('/starter-templates/')) {
|
|
3325
|
+
await handleStarterTemplates(req, res, pathname)
|
|
3326
|
+
return
|
|
3327
|
+
}
|
|
3187
3328
|
const editAgentMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)$/)
|
|
3188
3329
|
if (editAgentMatch) {
|
|
3189
3330
|
await handleEditAgent(req, res, decodeURIComponent(editAgentMatch[1]))
|