create-loopengine 0.1.12 → 0.1.13
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/adapters/http.ts +140 -10
- package/template/package.json +2 -2
package/package.json
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
AgentEditNotSupportedError,
|
|
44
44
|
AgentFileNotFoundError,
|
|
45
45
|
type AgentEditResult,
|
|
46
|
+
type AgentEditableFields,
|
|
46
47
|
createSessionStore,
|
|
47
48
|
createCheckpointStore,
|
|
48
49
|
type TurnCheckpoint,
|
|
@@ -79,6 +80,10 @@ import {
|
|
|
79
80
|
deleteSkill,
|
|
80
81
|
SkillInvalidIdError,
|
|
81
82
|
SkillNotFoundError,
|
|
83
|
+
listSkillgardenCatalog,
|
|
84
|
+
readSkillgardenCatalogEntry,
|
|
85
|
+
addSkillgardenSkillToAgent,
|
|
86
|
+
SkillgardenUnavailableError,
|
|
82
87
|
readActauthConfig,
|
|
83
88
|
addActauthRule,
|
|
84
89
|
updateActauthRule,
|
|
@@ -314,9 +319,14 @@ async function describeAgent(entry: RegistryEntry): Promise<Record<string, unkno
|
|
|
314
319
|
// configured for *this* agent, so the config page's own Skills tab
|
|
315
320
|
// deliberately doesn't mix it into skills/skillsDirs below.
|
|
316
321
|
const skillsDirs = config.skillsDirs ?? [`agents/${config.name}/skills`]
|
|
317
|
-
|
|
322
|
+
// Same defaults, same reasoning, as loopengine's own run-agent.ts
|
|
323
|
+
// SkillGarden/BudgetTracker construction — kept in sync there and here
|
|
324
|
+
// since this is what a "Skills"/"Limits & budgets" tab shows an
|
|
325
|
+
// operator, and it has to describe what runAgent() would actually
|
|
326
|
+
// resolve, not a second guess (see this function's own header comment).
|
|
327
|
+
const skillGarden = new SkillGarden({ dirs: skillsDirs, indexBudgetTokens: config.skillIndexBudgetTokens ?? 2000 })
|
|
318
328
|
const skillIndex = skillGarden.buildIndex().included
|
|
319
|
-
const systemSkillGarden = new SkillGarden({ dirs: [systemSkillsDir], indexBudgetTokens: config.skillIndexBudgetTokens ??
|
|
329
|
+
const systemSkillGarden = new SkillGarden({ dirs: [systemSkillsDir], indexBudgetTokens: config.skillIndexBudgetTokens ?? 2000 })
|
|
320
330
|
const systemSkillIndex = systemSkillGarden.buildIndex().included
|
|
321
331
|
|
|
322
332
|
return {
|
|
@@ -326,8 +336,8 @@ async function describeAgent(entry: RegistryEntry): Promise<Record<string, unkno
|
|
|
326
336
|
? { provider: config.model.provider, model: config.model.model, maxTokens: config.model.maxTokens }
|
|
327
337
|
: 'custom (module exports its own createModelCall)',
|
|
328
338
|
maxTurns: config.maxTurns ?? 25,
|
|
329
|
-
contextBudgetTokens: config.contextBudgetTokens ??
|
|
330
|
-
skillIndexBudgetTokens: config.skillIndexBudgetTokens ??
|
|
339
|
+
contextBudgetTokens: config.contextBudgetTokens ?? 100000,
|
|
340
|
+
skillIndexBudgetTokens: config.skillIndexBudgetTokens ?? 2000,
|
|
331
341
|
skillsDirs: skillsDirs,
|
|
332
342
|
skills: skillIndex.map((s) => ({ name: s.name, description: s.description })),
|
|
333
343
|
systemSkills: systemSkillIndex.map((s) => ({ name: s.name, description: s.description })),
|
|
@@ -644,6 +654,33 @@ function parseAgentTemplateOptions(body: Record<string, unknown>): { ok: true; v
|
|
|
644
654
|
return { ok: true, value: options }
|
|
645
655
|
}
|
|
646
656
|
|
|
657
|
+
// The Overview tab's "Limits & budgets" edit form — kept separate from
|
|
658
|
+
// parseAgentTemplateOptions since that one's shape (AgentTemplateOptions)
|
|
659
|
+
// is shared with handleCreateAgent/scaffoldAgent, which has no notion of
|
|
660
|
+
// these three at all (a freshly scaffolded agent always starts on
|
|
661
|
+
// loopengine's own defaults — see AgentConfig's own doc comments). Each
|
|
662
|
+
// of the three is optional and independent, same as systemPrompt/model
|
|
663
|
+
// above.
|
|
664
|
+
function parsePositiveInt(value: unknown, field: string): { ok: true; value: number | undefined } | { ok: false; error: string } {
|
|
665
|
+
if (value === undefined || value === null) return { ok: true, value: undefined }
|
|
666
|
+
if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
|
|
667
|
+
return { ok: false, error: `${field} must be a positive integer` }
|
|
668
|
+
}
|
|
669
|
+
return { ok: true, value }
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function parseAgentLimitsFields(
|
|
673
|
+
body: Record<string, unknown>,
|
|
674
|
+
): { ok: true; value: Pick<AgentEditableFields, 'maxTurns' | 'contextBudgetTokens' | 'skillIndexBudgetTokens'> } | { ok: false; error: string } {
|
|
675
|
+
const value: Pick<AgentEditableFields, 'maxTurns' | 'contextBudgetTokens' | 'skillIndexBudgetTokens'> = {}
|
|
676
|
+
for (const field of ['maxTurns', 'contextBudgetTokens', 'skillIndexBudgetTokens'] as const) {
|
|
677
|
+
const parsed = parsePositiveInt(body[field], field)
|
|
678
|
+
if (!parsed.ok) return parsed
|
|
679
|
+
if (parsed.value !== undefined) value[field] = parsed.value
|
|
680
|
+
}
|
|
681
|
+
return { ok: true, value }
|
|
682
|
+
}
|
|
683
|
+
|
|
647
684
|
async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
648
685
|
const body = await readJsonBody(req)
|
|
649
686
|
if (typeof body.name !== 'string' || !body.name) {
|
|
@@ -677,9 +714,11 @@ async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Pro
|
|
|
677
714
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ path: indexPath, registered: true }))
|
|
678
715
|
}
|
|
679
716
|
|
|
680
|
-
// Backs the Overview tab's System prompt / Model
|
|
681
|
-
//
|
|
682
|
-
//
|
|
717
|
+
// Backs the Overview tab's System prompt / Model / Limits & budgets edit
|
|
718
|
+
// forms — any subset of systemPrompt, model, maxTurns, contextBudgetTokens,
|
|
719
|
+
// skillIndexBudgetTokens can be sent independently (see
|
|
720
|
+
// parseAgentTemplateOptions/parseAgentLimitsFields), every other field
|
|
721
|
+
// stays untouched both on disk and live. Persists via
|
|
683
722
|
// agent-file-admin.ts's editAgentFile (see its own doc comment for why
|
|
684
723
|
// this is a surgical AST edit, not a full file regeneration), then
|
|
685
724
|
// applies the exact same resolved values to *this* running process via
|
|
@@ -699,14 +738,22 @@ async function handleEditAgent(req: IncomingMessage, res: ServerResponse, agentN
|
|
|
699
738
|
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsedOptions.error }))
|
|
700
739
|
return
|
|
701
740
|
}
|
|
702
|
-
|
|
703
|
-
|
|
741
|
+
const parsedLimits = parseAgentLimitsFields(body)
|
|
742
|
+
if (!parsedLimits.ok) {
|
|
743
|
+
res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsedLimits.error }))
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
const editFields: AgentEditableFields = { ...parsedOptions.value, ...parsedLimits.value }
|
|
747
|
+
if (Object.keys(editFields).length === 0) {
|
|
748
|
+
res
|
|
749
|
+
.writeHead(400, { 'content-type': 'application/json' })
|
|
750
|
+
.end(JSON.stringify({ error: 'systemPrompt, model, maxTurns, contextBudgetTokens, or skillIndexBudgetTokens is required' }))
|
|
704
751
|
return
|
|
705
752
|
}
|
|
706
753
|
|
|
707
754
|
let result: AgentEditResult
|
|
708
755
|
try {
|
|
709
|
-
result = editAgentFile(agentName,
|
|
756
|
+
result = editAgentFile(agentName, editFields)
|
|
710
757
|
} catch (err) {
|
|
711
758
|
const status =
|
|
712
759
|
err instanceof AgentEditNotSupportedError || err instanceof AgentModelError ? 400 : err instanceof AgentFileNotFoundError ? 404 : 500
|
|
@@ -716,6 +763,9 @@ async function handleEditAgent(req: IncomingMessage, res: ServerResponse, agentN
|
|
|
716
763
|
|
|
717
764
|
const configPatch: Partial<AgentConfig> = {}
|
|
718
765
|
if (result.systemPrompt !== undefined) configPatch.systemPrompt = result.systemPrompt
|
|
766
|
+
if (result.maxTurns !== undefined) configPatch.maxTurns = result.maxTurns
|
|
767
|
+
if (result.contextBudgetTokens !== undefined) configPatch.contextBudgetTokens = result.contextBudgetTokens
|
|
768
|
+
if (result.skillIndexBudgetTokens !== undefined) configPatch.skillIndexBudgetTokens = result.skillIndexBudgetTokens
|
|
719
769
|
let createModelCall: RegistryEntry['createModelCall'] | undefined
|
|
720
770
|
if (result.model) {
|
|
721
771
|
configPatch.model = result.model
|
|
@@ -782,6 +832,69 @@ function handleSkillDelete(res: ServerResponse, agentName: string, skillId: stri
|
|
|
782
832
|
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true }))
|
|
783
833
|
}
|
|
784
834
|
|
|
835
|
+
// Backs the Skills tab's "Browse skillgarden catalog" section — a
|
|
836
|
+
// read-only, agent-independent list of everything skillgarden ships in
|
|
837
|
+
// its own bundled registry (see loopengine's own web/skillgarden-admin.ts).
|
|
838
|
+
// Not wrapped in a per-agent :name segment since the catalog itself
|
|
839
|
+
// isn't scoped to any one agent; only the POST below (actually
|
|
840
|
+
// installing an entry) is.
|
|
841
|
+
function handleSkillgardenCatalogGet(res: ServerResponse): void {
|
|
842
|
+
try {
|
|
843
|
+
// Computed before writeHead, not inlined into .end()'s own argument —
|
|
844
|
+
// res.writeHead(200, ...) commits the status line the instant it's
|
|
845
|
+
// called, so a throw evaluated *after* it (as it would be if it were
|
|
846
|
+
// inlined as .end()'s argument) leaves headers already sent as 200
|
|
847
|
+
// with no body, and the catch block's own writeHead(status, ...)
|
|
848
|
+
// then fails outright (ERR_HTTP_HEADERS_SENT) instead of ever
|
|
849
|
+
// reaching the client.
|
|
850
|
+
const catalog = listSkillgardenCatalog()
|
|
851
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(catalog))
|
|
852
|
+
} catch (err) {
|
|
853
|
+
const status = err instanceof SkillgardenUnavailableError ? 503 : 500
|
|
854
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Full SKILL.md body for the catalog's own "View" toggle — kept as a
|
|
859
|
+
// separate route from the list above (rather than inlining every body
|
|
860
|
+
// into it) for the same reason the Skills tab's own edit form fetches a
|
|
861
|
+
// skill's body lazily instead of shipping it in the initial list.
|
|
862
|
+
function handleSkillgardenCatalogEntryGet(res: ServerResponse, category: string, id: string): void {
|
|
863
|
+
try {
|
|
864
|
+
// See handleSkillgardenCatalogGet's own comment for why this is
|
|
865
|
+
// computed before writeHead rather than inlined into .end()'s
|
|
866
|
+
// argument.
|
|
867
|
+
const entry = readSkillgardenCatalogEntry(category, id)
|
|
868
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(entry))
|
|
869
|
+
} catch (err) {
|
|
870
|
+
const status = err instanceof SkillgardenUnavailableError ? 503 : 404
|
|
871
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }))
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Installs a catalog entry into agents/:name/skills/ — delegates to
|
|
876
|
+
// skillgarden's own addSkill (see skillgarden-admin.ts), which throws
|
|
877
|
+
// plain Errors (no typed classes) for "unknown skill"/"already exists",
|
|
878
|
+
// so those two are told apart by message shape rather than instanceof,
|
|
879
|
+
// same as any other dependency that doesn't export typed errors for its
|
|
880
|
+
// own failure modes.
|
|
881
|
+
async function handleSkillgardenCatalogAdd(req: IncomingMessage, res: ServerResponse, agentName: string, category: string, id: string): Promise<void> {
|
|
882
|
+
if (!getEntry(agentName)) {
|
|
883
|
+
res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
|
|
884
|
+
return
|
|
885
|
+
}
|
|
886
|
+
const body = await readJsonBody(req)
|
|
887
|
+
const force = body.force === true
|
|
888
|
+
try {
|
|
889
|
+
const result = addSkillgardenSkillToAgent(agentName, category, id, force)
|
|
890
|
+
res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ ok: true, id: result.id }))
|
|
891
|
+
} catch (err) {
|
|
892
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
893
|
+
const status = err instanceof SkillgardenUnavailableError ? 503 : /already exists/.test(message) ? 409 : /^Unknown skill/.test(message) ? 404 : 500
|
|
894
|
+
res.writeHead(status, { 'content-type': 'application/json' }).end(JSON.stringify({ error: message }))
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
785
898
|
// Backs the Actauth tab's rule editor — parses a rule body shared by both
|
|
786
899
|
// the add (POST) and update (PUT, minus `name`) routes below.
|
|
787
900
|
function parseActauthRuleBody(body: Record<string, unknown>, requireName: boolean): { ok: true; value: { name: string; scope: string; tool: string; decision: Decision } } | { ok: false; error: string } {
|
|
@@ -1906,6 +2019,23 @@ const server = createServer(async (req, res) => {
|
|
|
1906
2019
|
return
|
|
1907
2020
|
}
|
|
1908
2021
|
|
|
2022
|
+
// Backs the Skills tab's "Browse skillgarden catalog" section — see
|
|
2023
|
+
// the three handlers above it.
|
|
2024
|
+
if (req.method === 'GET' && pathname === '/skillgarden-catalog') {
|
|
2025
|
+
handleSkillgardenCatalogGet(res)
|
|
2026
|
+
return
|
|
2027
|
+
}
|
|
2028
|
+
const skillgardenEntryMatch = req.method === 'GET' && pathname.match(/^\/skillgarden-catalog\/([^/]+)\/([^/]+)$/)
|
|
2029
|
+
if (skillgardenEntryMatch) {
|
|
2030
|
+
handleSkillgardenCatalogEntryGet(res, decodeURIComponent(skillgardenEntryMatch[1]), decodeURIComponent(skillgardenEntryMatch[2]))
|
|
2031
|
+
return
|
|
2032
|
+
}
|
|
2033
|
+
const skillgardenAddMatch = req.method === 'POST' && pathname.match(/^\/agents\/([^/]+)\/skillgarden-catalog\/([^/]+)\/([^/]+)$/)
|
|
2034
|
+
if (skillgardenAddMatch) {
|
|
2035
|
+
await handleSkillgardenCatalogAdd(req, res, decodeURIComponent(skillgardenAddMatch[1]), decodeURIComponent(skillgardenAddMatch[2]), decodeURIComponent(skillgardenAddMatch[3]))
|
|
2036
|
+
return
|
|
2037
|
+
}
|
|
2038
|
+
|
|
1909
2039
|
// Backs the Actauth tab's rule editor and default_decision control.
|
|
1910
2040
|
// The default-decision route is checked before the :ruleName routes
|
|
1911
2041
|
// right below since 'default-decision' would otherwise itself match
|
package/template/package.json
CHANGED