create-loopengine 0.3.1 → 0.3.3

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.3.1",
3
+ "version": "0.3.3",
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",
@@ -67,6 +67,7 @@ import {
67
67
  type AgentConfig,
68
68
  type AgentModelConfig,
69
69
  type ToolDefinition,
70
+ type Message,
70
71
  type ModelContentBlock,
71
72
  type RunAgentResult,
72
73
  playgroundHtml,
@@ -120,6 +121,8 @@ import {
120
121
  agentDirFor,
121
122
  verifyProxyIdentity,
122
123
  TRUSTED_PROXY_IDENTITY_HEADER,
124
+ reportRun,
125
+ usedTool,
123
126
  NotifierFileError,
124
127
  verifyDecisionToken,
125
128
  decidePath,
@@ -646,7 +649,11 @@ async function describeAgent(entry: RegistryEntry): Promise<Record<string, unkno
646
649
  // tab badge; the Apps tab's own GET .../abilities has the detail
647
650
  // (and its slower npm version lookups).
648
651
  abilityCount: listInstalledAbilities(config.name).length,
649
- skills: skillIndex.map((s) => ({ name: s.name, description: s.description })),
652
+ // A skill that's a workflow is edited as one, on the Workflows tab.
653
+ skills: (() => {
654
+ const workflowIds = new Set(readWorkflows(agentDirFor(config)).map((w) => w.id))
655
+ return skillIndex.map((s) => ({ name: s.name, description: s.description, ...(workflowIds.has(s.name) ? { workflow: true } : {}) }))
656
+ })(),
650
657
  systemSkills: systemSkillIndex.map((s) => ({ name: s.name, description: s.description })),
651
658
  tools: tools.map(describeTool),
652
659
  // createAskUserTool() with no onPending, purely to describe its
@@ -1925,6 +1932,8 @@ async function executeUnattendedRun(agent: string, message: string, rawSessionId
1925
1932
  async function reportWorkflowRun(run: WorkflowRun): Promise<void> {
1926
1933
  const event: RunCallbackEvent | undefined = run.status === 'done' ? 'run.done' : run.status === 'error' ? 'run.failed' : run.status === 'awaiting_approval' ? 'run.awaiting_approval' : undefined
1927
1934
  if (!event) return
1935
+ // For a host's run observer (core/run-events.ts); a failed run isn't reported.
1936
+ if (run.status !== 'error') reportRun({ agent: run.agent, source: 'workflow', id: run.run_id, via: run.source, usedTool: true })
1928
1937
  const entry = getEntry(run.agent)
1929
1938
  if (run.notify && entry && event !== 'run.awaiting_approval') {
1930
1939
  const text = run.status === 'done' ? `Workflow ${run.workflow_id} for ${run.agent} ran:\n${run.result ?? ''}` : `Workflow ${run.workflow_id} for ${run.agent} failed: ${run.error ?? 'unknown error'}`
@@ -1942,7 +1951,12 @@ const workflowRunner = new WorkflowRunner({ execute: executeUnattendedRun, onCha
1942
1951
  // A task that runs a workflow is also recorded as a run of that workflow,
1943
1952
  // so the workflow's Recent runs shows its scheduled runs too.
1944
1953
  function fireScheduledTask(task: ScheduledTask, rawSessionId: string): ReturnType<typeof executeUnattendedRun> {
1945
- if (!task.workflow_id) return executeUnattendedRun(task.agent, task.message, rawSessionId)
1954
+ if (!task.workflow_id) {
1955
+ return executeUnattendedRun(task.agent, task.message, rawSessionId).then((outcome) => {
1956
+ reportRun({ agent: task.agent, source: 'schedule', id: randomUUID(), usedTool: true })
1957
+ return outcome
1958
+ })
1959
+ }
1946
1960
  return workflowRunner.runForSchedule({
1947
1961
  agent: task.agent,
1948
1962
  workflow_id: task.workflow_id,
@@ -1999,12 +2013,14 @@ async function handleSchedulesGet(res: ServerResponse, agentName: string): Promi
1999
2013
  if (!t.workflow_id) return t
2000
2014
  const workflow = workflows.find((w) => w.id === t.workflow_id)
2001
2015
  // Its start message as this task's runs see it, parameters filled in.
2002
- const startMessage = workflow && fillStartMessage(workflow.startMessage, t.params ?? {}, Object.keys(workflow.parameters))
2003
- return workflow ? { ...t, workflow: { id: workflow.id, title: workflow.title, start_message: startMessage } } : { ...t, workflow_missing: true }
2016
+ // An older workflow's start message, else its instructions' first line.
2017
+ const lead = workflow && (workflow.startMessage || workflow.instructions.split('\n').find((line) => line.trim()) || '')
2018
+ const startMessage = workflow && fillStartMessage(lead!, t.params ?? {}, Object.keys(workflow.parameters))
2019
+ return workflow ? { ...t, workflow: { id: workflow.id, title: workflow.title, description: workflow.description, start_message: startMessage } } : { ...t, workflow_missing: true }
2004
2020
  })
2005
2021
  scheduleJson(res, 200, {
2006
2022
  tasks,
2007
- workflows: workflows.map((w) => ({ id: w.id, title: w.title, description: w.description, start_message: w.startMessage, parameters: w.parameters })),
2023
+ workflows: workflows.map((w) => ({ id: w.id, title: w.title, description: w.description, start_message: w.startMessage || w.instructions.split('\n').find((line) => line.trim()) || '', parameters: w.parameters })),
2008
2024
  time_zone: workspaceTimeZone(),
2009
2025
  })
2010
2026
  }
@@ -2917,6 +2933,19 @@ async function raceAndRespond(res: ServerResponse, agentName: string, rawSession
2917
2933
  res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify(responseBody))
2918
2934
  }
2919
2935
 
2936
+ // Who sent a message, as a trusted proxy says in its signed identity:
2937
+ // an API key (role "api") or a signed-in person. Without a proxy, unknown.
2938
+ function messageCaller(req: IncomingMessage): 'api' | 'console' | 'unknown' {
2939
+ const identity = trustedProxySecret ? verifyProxyIdentity(req.headers[TRUSTED_PROXY_IDENTITY_HEADER], trustedProxySecret) : undefined
2940
+ if (!identity) return 'unknown'
2941
+ return identity.role === 'api' ? 'api' : 'console'
2942
+ }
2943
+
2944
+ // For a host's run observer (core/run-events.ts).
2945
+ function reportMessageRun(req: IncomingMessage, agent: string, newMessages: Message[]): void {
2946
+ reportRun({ agent, source: 'message', id: randomUUID(), caller: messageCaller(req), usedTool: usedTool(newMessages) })
2947
+ }
2948
+
2920
2949
  async function handleMessages(req: IncomingMessage, res: ServerResponse, agentName: string): Promise<void> {
2921
2950
  const parsed = await parseRequest(req, agentName)
2922
2951
  if (!parsed.ok) {
@@ -2982,6 +3011,7 @@ async function handleMessages(req: IncomingMessage, res: ServerResponse, agentNa
2982
3011
  pushSignal(state, { type: 'question', entry: question })
2983
3012
  },
2984
3013
  })
3014
+ reportMessageRun(req, agentName, result.newMessages)
2985
3015
  if (result.pending) {
2986
3016
  await createCheckpointFromPending(agentName, tenant, rawSessionId, result.pending)
2987
3017
  }
@@ -3092,6 +3122,7 @@ async function handleMessagesStream(req: IncomingMessage, res: ServerResponse, a
3092
3122
  pushSignal(state, { type: 'question', entry: question })
3093
3123
  },
3094
3124
  })
3125
+ reportMessageRun(req, agentName, result.newMessages)
3095
3126
  if (result.pending) {
3096
3127
  await createCheckpointFromPending(agentName, tenant, rawSessionId, result.pending)
3097
3128
  }
@@ -3963,7 +3994,7 @@ function jobDeps(agentName: string, entry: RegistryEntry, connector: MessagingCo
3963
3994
  connector,
3964
3995
  plan: process.env.LOOPENGINE_PLAN,
3965
3996
  allowed: (message) => jobAllowed(entry.config, app, message),
3966
- runTurn: (sessionId, text) => runJobTurn(agentName, sessionId, text),
3997
+ runTurn: (sessionId, text) => runJobTurn(agentName, sessionId, text, connector.app === 'telegram' ? 'telegram' : 'slack'),
3967
3998
  decideLink: (pendingId, kind) => {
3968
3999
  const base = publicBaseUrl(connectionPublicUrl(entry.config, app))
3969
4000
  return base ? `${base}${decidePath(pendingId, kind)}` : undefined
@@ -3998,7 +4029,7 @@ function jobTurnOf(text: string, pending: PendingSummary[] | undefined): JobTurn
3998
4029
  }
3999
4030
 
4000
4031
  // One turn of a job thread: the same session for the whole thread.
4001
- async function runJobTurn(agent: string, rawSessionId: string, message: string): Promise<JobTurn> {
4032
+ async function runJobTurn(agent: string, rawSessionId: string, message: string, source: 'slack' | 'telegram'): Promise<JobTurn> {
4002
4033
  const entry = getEntry(agent)
4003
4034
  if (!entry) throw new Error(`no agent named "${agent}" in this project`)
4004
4035
  const tenant = 'default'
@@ -4011,6 +4042,8 @@ async function runJobTurn(agent: string, rawSessionId: string, message: string):
4011
4042
  approver: threadApprover,
4012
4043
  questionHandler: threadQuestions,
4013
4044
  })
4045
+ // For a host's run observer (core/run-events.ts).
4046
+ reportRun({ agent, source, id: randomUUID(), usedTool: usedTool(result.newMessages) })
4014
4047
  if (result.pending) await createCheckpointFromPending(agent, tenant, rawSessionId, result.pending)
4015
4048
  return { newMessages: result.newMessages, result: { text: result.text, pending: pendingSummary(result.pending) } }
4016
4049
  })
@@ -9,7 +9,7 @@
9
9
  "cli": "tsx adapters/cli.ts"
10
10
  },
11
11
  "dependencies": {
12
- "loopengine": "^0.3.3",
12
+ "loopengine": "^0.3.8",
13
13
  "actauth": "^0.0.12",
14
14
  "tsx": "^4.19.0",
15
15
  "typescript": "^5.7.0"