create-loopengine 0.1.71 → 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 +1 -1
- package/template/_gitignore +2 -0
- package/template/adapters/http.ts +95 -0
- package/template/package.json +1 -1
package/package.json
CHANGED
package/template/_gitignore
CHANGED
|
@@ -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,
|
|
@@ -135,6 +149,7 @@ import {
|
|
|
135
149
|
type DecisionOutcome,
|
|
136
150
|
type MessagingDeps,
|
|
137
151
|
handleMessagingAction,
|
|
152
|
+
settleThreadChoice,
|
|
138
153
|
describeChange,
|
|
139
154
|
openSlackAnswerModal,
|
|
140
155
|
parseSlackInteraction,
|
|
@@ -878,6 +893,71 @@ function parseAgentLimitsFields(
|
|
|
878
893
|
return { ok: true, value }
|
|
879
894
|
}
|
|
880
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
|
+
|
|
881
961
|
async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
882
962
|
const body = await readJsonBody(req)
|
|
883
963
|
if (typeof body.name !== 'string' || !body.name) {
|
|
@@ -2803,6 +2883,12 @@ async function handleSlackInteraction(req: IncomingMessage, res: ServerResponse,
|
|
|
2803
2883
|
if (interaction.type === 'decide') await resolvePendingApproval(capture.res, interaction.pendingId, { decision: interaction.decision })
|
|
2804
2884
|
else await answerPendingQuestion(capture.res, interaction.pendingId, { answer: interaction.answer })
|
|
2805
2885
|
const outcome = await capture.done
|
|
2886
|
+
// A proposed change or a Confirm still waiting in the thread no longer
|
|
2887
|
+
// applies: replace it so its buttons don't linger.
|
|
2888
|
+
if (botToken && outcome.status < 400 && !outcome.body.alreadyResolved) {
|
|
2889
|
+
const done = interaction.type === 'decide' ? (interaction.decision === 'approve' ? 'Approved' : 'Denied') : 'Answered'
|
|
2890
|
+
await settleThreadChoice(slackMessagingDeps(agentName, entry, botToken), interaction.pendingId, `${done}${interaction.user ? ` by <@${interaction.user}>` : ''} with the request's own buttons.`).catch((err) => console.error('[loopengine] Slack update failed:', err))
|
|
2891
|
+
}
|
|
2806
2892
|
if (interaction.responseUrl) {
|
|
2807
2893
|
let text = slackOutcomeText(interaction.type === 'decide' ? interaction.decision : 'answer', outcome.status, outcome.body, interaction.user)
|
|
2808
2894
|
if (ran && outcome.status < 400 && !outcome.body.alreadyResolved) text += ran
|
|
@@ -3177,6 +3263,15 @@ const server = createServer(async (req, res) => {
|
|
|
3177
3263
|
await handleCreateAgent(req, res)
|
|
3178
3264
|
return
|
|
3179
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
|
+
}
|
|
3180
3275
|
const editAgentMatch = req.method === 'PUT' && pathname.match(/^\/agents\/([^/]+)$/)
|
|
3181
3276
|
if (editAgentMatch) {
|
|
3182
3277
|
await handleEditAgent(req, res, decodeURIComponent(editAgentMatch[1]))
|