create-loopengine 0.1.39 → 0.1.42

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,13 +1,13 @@
1
1
  {
2
2
  "name": "create-loopengine",
3
- "version": "0.1.39",
3
+ "version": "0.1.42",
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",
7
7
  "homepage": "https://loopengine.co",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "git+https://github.com/evanyan22/create-loopengine.git"
10
+ "url": "git+https://github.com/loopengine-co/create-loopengine.git"
11
11
  },
12
12
  "bin": {
13
13
  "create-loopengine": "dist/cli.js"
@@ -64,6 +64,7 @@ import {
64
64
  systemTools,
65
65
  systemSkillsDir,
66
66
  type AgentConfig,
67
+ type AgentModelConfig,
67
68
  type ToolDefinition,
68
69
  type ModelContentBlock,
69
70
  type RunAgentResult,
@@ -356,7 +357,16 @@ async function describeAgent(entry: RegistryEntry): Promise<Record<string, unkno
356
357
  name: config.name,
357
358
  systemPrompt: config.systemPrompt,
358
359
  model: config.model
359
- ? { provider: config.model.provider, model: config.model.model, maxTokens: config.model.maxTokens }
360
+ ? {
361
+ provider: config.model.provider,
362
+ model: config.model.model,
363
+ maxTokens: config.model.maxTokens,
364
+ // Only the 'openai' variant of AgentModelConfig carries this —
365
+ // an `in` check rather than a cast, so a provider that can
366
+ // never have it just reports undefined instead of needing its
367
+ // own narrowing branch here.
368
+ reasoningEffort: 'reasoningEffort' in config.model ? config.model.reasoningEffort : undefined,
369
+ }
360
370
  : 'custom (module exports its own createModelCall)',
361
371
  maxTurns: config.maxTurns ?? 25,
362
372
  contextBudgetTokens: config.contextBudgetTokens ?? 100000,
@@ -830,6 +840,33 @@ async function handleCreateAgent(req: IncomingMessage, res: ServerResponse): Pro
830
840
  // why that's regenerable on its own, unlike the rest of an already-
831
841
  // imported module). No restart needed, same as every other admin edit
832
842
  // in this app.
843
+ // model.maxTokens/model.reasoningEffort are edit-only — scaffoldAgent has
844
+ // no use for either at creation time, so they're parsed here rather than
845
+ // widening parseAgentTemplateOptions/AgentTemplateOptions (shared with
846
+ // agent creation) for something only this route needs. Only ever called
847
+ // after parseAgentTemplateOptions already validated provider/model, so
848
+ // there's nothing to re-validate here beyond these two fields themselves.
849
+ function parseEditModelExtras(
850
+ body: Record<string, unknown>,
851
+ model: AgentEditableFields['model'],
852
+ ): { ok: true; value: AgentEditableFields['model'] } | { ok: false; error: string } {
853
+ if (!model || typeof body.model !== 'object' || body.model === null) return { ok: true, value: model }
854
+ const rawModel = body.model as Record<string, unknown>
855
+ if (rawModel.maxTokens !== undefined && rawModel.maxTokens !== null) {
856
+ if (typeof rawModel.maxTokens !== 'number' || !Number.isInteger(rawModel.maxTokens) || rawModel.maxTokens < 1) {
857
+ return { ok: false, error: 'model.maxTokens must be a positive integer' }
858
+ }
859
+ model.maxTokens = rawModel.maxTokens
860
+ }
861
+ if (rawModel.reasoningEffort !== undefined && rawModel.reasoningEffort !== null) {
862
+ if (typeof rawModel.reasoningEffort !== 'string') {
863
+ return { ok: false, error: 'model.reasoningEffort must be a string' }
864
+ }
865
+ model.reasoningEffort = rawModel.reasoningEffort
866
+ }
867
+ return { ok: true, value: model }
868
+ }
869
+
833
870
  async function handleEditAgent(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
834
871
  if (!getEntry(agentName)) {
835
872
  res.writeHead(404, { 'content-type': 'application/json' }).end(JSON.stringify({ error: `unknown agent '${agentName}'` }))
@@ -841,12 +878,17 @@ async function handleEditAgent(req: IncomingMessage, res: ServerResponse, agentN
841
878
  res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsedOptions.error }))
842
879
  return
843
880
  }
881
+ const parsedModelExtras = parseEditModelExtras(body, parsedOptions.value.model)
882
+ if (!parsedModelExtras.ok) {
883
+ res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsedModelExtras.error }))
884
+ return
885
+ }
844
886
  const parsedLimits = parseAgentLimitsFields(body)
845
887
  if (!parsedLimits.ok) {
846
888
  res.writeHead(400, { 'content-type': 'application/json' }).end(JSON.stringify({ error: parsedLimits.error }))
847
889
  return
848
890
  }
849
- const editFields: AgentEditableFields = { ...parsedOptions.value, ...parsedLimits.value }
891
+ const editFields: AgentEditableFields = { ...parsedOptions.value, model: parsedModelExtras.value, ...parsedLimits.value }
850
892
  if (Object.keys(editFields).length === 0) {
851
893
  res
852
894
  .writeHead(400, { 'content-type': 'application/json' })
@@ -871,8 +913,27 @@ async function handleEditAgent(req: IncomingMessage, res: ServerResponse, agentN
871
913
  if (result.skillIndexBudgetTokens !== undefined) configPatch.skillIndexBudgetTokens = result.skillIndexBudgetTokens
872
914
  let createModelCall: RegistryEntry['createModelCall'] | undefined
873
915
  if (result.model) {
874
- configPatch.model = result.model
875
- createModelCall = await synthesizeCreateModelCall(result.model)
916
+ // editAgentFile's own AgentEditResult.model is one flat shape across
917
+ // every provider (see that file's own reasoningEffort-only-for-openai
918
+ // check) — AgentModelConfig is a discriminated union instead, so
919
+ // reasoningEffort only ever type-checks on the 'openai' branch.
920
+ // reasoningEffort itself is narrowed no further than `string` at this
921
+ // boundary (same as `model` — an arbitrary provider-defined name, not
922
+ // an enum this layer validates); an invalid value surfaces as a clear
923
+ // rejection from the provider's own API, same as an invalid model name
924
+ // already does.
925
+ const m = result.model
926
+ const modelConfig: AgentModelConfig =
927
+ m.provider === 'openai'
928
+ ? {
929
+ provider: 'openai',
930
+ model: m.model,
931
+ maxTokens: m.maxTokens,
932
+ reasoningEffort: m.reasoningEffort as Extract<AgentModelConfig, { provider: 'openai' }>['reasoningEffort'],
933
+ }
934
+ : { provider: m.provider, model: m.model, maxTokens: m.maxTokens }
935
+ configPatch.model = modelConfig
936
+ createModelCall = await synthesizeCreateModelCall(modelConfig)
876
937
  }
877
938
  updateAgent(agentName, { config: configPatch, createModelCall })
878
939
 
@@ -2431,6 +2492,14 @@ const server = createServer(async (req, res) => {
2431
2492
 
2432
2493
  await handleMessages(req, res, decodeURIComponent(match[1]))
2433
2494
  } catch (err) {
2495
+ // headersSent means some route already called writeHead (200,
2496
+ // usually) before an awaited call after it threw — the exact
2497
+ // "Unexpected end of JSON input" symptom on the client, since
2498
+ // res.end() here with no argument sends an empty body under
2499
+ // already-committed headers. Logged, not just silently ended, so
2500
+ // that symptom is actually diagnosable from the server side instead
2501
+ // of a client-side error with no server-side trace at all.
2502
+ console.error('[loopengine] unhandled error in request handler:', err)
2434
2503
  if (res.headersSent) {
2435
2504
  res.end()
2436
2505
  } else {