loopengine 0.1.2 → 0.1.4
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/dist/core/agent-config.d.ts +9 -5
- package/dist/core/durable-approvals.d.ts +106 -0
- package/dist/core/durable-approvals.js +158 -0
- package/dist/core/gateway-tools.js +10 -9
- package/dist/core/run-agent.js +20 -3
- package/dist/index.d.ts +24 -1
- package/dist/index.js +26 -1
- package/dist/web/actauth-admin.d.ts +36 -0
- package/dist/web/actauth-admin.js +108 -0
- package/dist/web/agent-file-admin.d.ts +30 -0
- package/dist/web/agent-file-admin.js +165 -0
- package/dist/web/agents-config-page.d.ts +1 -0
- package/dist/web/agents-config-page.js +1628 -0
- package/dist/web/agents-list-page.d.ts +1 -0
- package/dist/web/agents-list-page.js +246 -0
- package/dist/web/dev-ui-styles.d.ts +1 -0
- package/dist/web/dev-ui-styles.js +63 -0
- package/dist/web/global-config-page.d.ts +1 -0
- package/dist/web/global-config-page.js +303 -0
- package/dist/web/global-config.d.ts +57 -0
- package/dist/web/global-config.js +67 -0
- package/dist/web/playground.d.ts +1 -0
- package/dist/web/playground.js +1271 -0
- package/dist/web/skills-admin.d.ts +23 -0
- package/dist/web/skills-admin.js +67 -0
- package/dist/web/web-approver.d.ts +35 -0
- package/dist/web/web-approver.js +88 -0
- package/package.json +5 -2
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Lets an operator edit an already-registered agent's systemPrompt/model
|
|
2
|
+
// directly from /agents/config's Overview tab, persisting the change to
|
|
3
|
+
// agents/<name>/index.ts itself — unlike gateway-tools.yml/actauth.yml/
|
|
4
|
+
// SKILL.md (plain data, safe to fully regenerate or edit via a
|
|
5
|
+
// dedicated Document API), an agent's index.ts is real TypeScript source
|
|
6
|
+
// that can also carry custom imports, a hand-written createModelCall,
|
|
7
|
+
// other AgentConfig fields, and comments an operator cares about. This
|
|
8
|
+
// edits via the TypeScript compiler API's AST plus exact source
|
|
9
|
+
// character offsets — the closest equivalent this repo has to `yaml`'s
|
|
10
|
+
// own Document API (parse -> locate the exact node -> splice its source
|
|
11
|
+
// text -> nothing else in the file changes) for TS source, since there's
|
|
12
|
+
// no ready-made "TS Document API" the way there is for YAML.
|
|
13
|
+
//
|
|
14
|
+
// Deliberately conservative: refuses, with a clear error, to touch a
|
|
15
|
+
// field whose *current* value isn't a plain, recognizable shape
|
|
16
|
+
// (systemPrompt: a string literal; model: an inline object literal with
|
|
17
|
+
// string-literal provider/model) rather than guess at rewriting anything
|
|
18
|
+
// more complex — a hand-customized agent (a template literal
|
|
19
|
+
// systemPrompt, a custom createModelCall instead of config.model, ...)
|
|
20
|
+
// is exactly the case this must never touch blindly.
|
|
21
|
+
//
|
|
22
|
+
// Deliberately lives outside bin/cli.ts, even though it reuses bin/cli.ts's own
|
|
23
|
+
// AgentModelError/Provider shape: bin/cli.ts is published as loopengine's
|
|
24
|
+
// own CLI binary (dist/bin/cli.js — see package.json's own "files" list),
|
|
25
|
+
// and this module's use of the `typescript` package (a devDependency,
|
|
26
|
+
// not a real dependency, of this package) would risk breaking
|
|
27
|
+
// `npx loopengine <command>` for any consumer whose own project doesn't
|
|
28
|
+
// happen to also have typescript installed. This module (like
|
|
29
|
+
// skills-admin.ts/actauth-admin.ts/global-config.ts before it) is only
|
|
30
|
+
// ever imported by adapters/http.ts, which itself is never published as
|
|
31
|
+
// part of the loopengine package — it's project-owned source a
|
|
32
|
+
// scaffolded project gets its own copy of, where `typescript` is already
|
|
33
|
+
// a guaranteed devDependency (see create-loopengine's own template
|
|
34
|
+
// package.json).
|
|
35
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
36
|
+
import { join } from 'node:path';
|
|
37
|
+
import ts from 'typescript';
|
|
38
|
+
import { agentDir } from '../core/gateway-tools.js';
|
|
39
|
+
import { AgentModelError } from '../bin/cli.js';
|
|
40
|
+
export class AgentEditNotSupportedError extends Error {
|
|
41
|
+
}
|
|
42
|
+
export class AgentFileNotFoundError extends Error {
|
|
43
|
+
}
|
|
44
|
+
const MODEL_ENV_VAR = {
|
|
45
|
+
anthropic: 'ANTHROPIC_API_KEY',
|
|
46
|
+
openai: 'OPENAI_API_KEY',
|
|
47
|
+
deepseek: 'DEEPSEEK_API_KEY',
|
|
48
|
+
};
|
|
49
|
+
// The exact shape agentIndexTemplate (bin/cli.ts) always generates for the
|
|
50
|
+
// model field's trailing comment — used to recognize a comment this
|
|
51
|
+
// tooling itself wrote (safe to correct) versus anything else an
|
|
52
|
+
// operator might have hand-written there (left alone — see
|
|
53
|
+
// updateModelComment's own doc comment).
|
|
54
|
+
const MODEL_COMMENT_PATTERN = /^\/\/\s*reads\s+[A-Z_]+$/;
|
|
55
|
+
// Escapes a value for safe interpolation into a single-quoted TS string
|
|
56
|
+
// literal in generated code — same reasoning (and same escaping) as
|
|
57
|
+
// bin/cli.ts's own tsStringLiteral, duplicated rather than imported since
|
|
58
|
+
// bin/cli.ts's copy is a private, unexported helper and this is the only
|
|
59
|
+
// other place that needs it.
|
|
60
|
+
function tsStringLiteral(value) {
|
|
61
|
+
return "'" + value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n') + "'";
|
|
62
|
+
}
|
|
63
|
+
// ts.getTrailingCommentRanges only looks for a comment starting exactly
|
|
64
|
+
// at `pos`, skipping whitespace but not other tokens — a trailing comma
|
|
65
|
+
// right after the model property's own end (the common case: model
|
|
66
|
+
// usually isn't the last property) sits in the way, so this retries one
|
|
67
|
+
// character further in if the first attempt found nothing and that
|
|
68
|
+
// character is a comma. Confirmed both cases live against a real parse
|
|
69
|
+
// before landing this — `getTrailingCommentRanges` genuinely returns
|
|
70
|
+
// undefined at the property's own end when a comma follows.
|
|
71
|
+
function trailingCommentAfter(source, pos) {
|
|
72
|
+
const ranges = ts.getTrailingCommentRanges(source, pos) ?? (source[pos] === ',' ? ts.getTrailingCommentRanges(source, pos + 1) : undefined);
|
|
73
|
+
return ranges?.[0];
|
|
74
|
+
}
|
|
75
|
+
function findConfigObjectLiteral(sourceFile) {
|
|
76
|
+
for (const stmt of sourceFile.statements) {
|
|
77
|
+
if (!ts.isVariableStatement(stmt))
|
|
78
|
+
continue;
|
|
79
|
+
if (!stmt.modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword))
|
|
80
|
+
continue;
|
|
81
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
82
|
+
if (ts.isIdentifier(decl.name) && decl.name.text === 'config' && decl.initializer && ts.isObjectLiteralExpression(decl.initializer)) {
|
|
83
|
+
return decl.initializer;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
function findProperty(obj, name) {
|
|
90
|
+
return obj.properties.find((p) => p.name !== undefined && ts.isIdentifier(p.name) && p.name.text === name);
|
|
91
|
+
}
|
|
92
|
+
/** Surgically rewrites just the fields given in `fields` — see this
|
|
93
|
+
* file's own header comment for why, and for the "refuse rather than
|
|
94
|
+
* guess" rule this follows for anything that isn't a plain, recognizable
|
|
95
|
+
* shape. Returns the values actually written (model's own default
|
|
96
|
+
* resolution included), so the caller (adapters/http.ts's
|
|
97
|
+
* handleEditAgent) can apply the exact same values to the *live*
|
|
98
|
+
* registry entry — see core/agent-registry.ts's own updateAgent — without
|
|
99
|
+
* needing to re-derive them. A field omitted from `fields` entirely is
|
|
100
|
+
* left completely untouched, both on disk and in the return value. */
|
|
101
|
+
export function editAgentFile(agentName, fields) {
|
|
102
|
+
const indexPath = join(agentDir(agentName), 'index.ts');
|
|
103
|
+
let source;
|
|
104
|
+
try {
|
|
105
|
+
source = readFileSync(indexPath, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new AgentFileNotFoundError(`agents/${agentName}/index.ts not found.`);
|
|
109
|
+
}
|
|
110
|
+
const sourceFile = ts.createSourceFile(indexPath, source, ts.ScriptTarget.Latest, true);
|
|
111
|
+
const configObj = findConfigObjectLiteral(sourceFile);
|
|
112
|
+
if (!configObj) {
|
|
113
|
+
throw new AgentEditNotSupportedError('Could not find "export const config: AgentConfig = { ... }" in this file — edit it directly.');
|
|
114
|
+
}
|
|
115
|
+
const edits = [];
|
|
116
|
+
const result = {};
|
|
117
|
+
if (fields.systemPrompt !== undefined) {
|
|
118
|
+
const prop = findProperty(configObj, 'systemPrompt');
|
|
119
|
+
if (!prop || !ts.isPropertyAssignment(prop) || !ts.isStringLiteral(prop.initializer)) {
|
|
120
|
+
throw new AgentEditNotSupportedError('systemPrompt in this file is not a plain string literal — edit it directly.');
|
|
121
|
+
}
|
|
122
|
+
edits.push({ start: prop.initializer.getStart(sourceFile), end: prop.initializer.getEnd(), text: tsStringLiteral(fields.systemPrompt) });
|
|
123
|
+
result.systemPrompt = fields.systemPrompt;
|
|
124
|
+
}
|
|
125
|
+
if (fields.model !== undefined) {
|
|
126
|
+
const prop = findProperty(configObj, 'model');
|
|
127
|
+
if (!prop || !ts.isPropertyAssignment(prop) || !ts.isObjectLiteralExpression(prop.initializer)) {
|
|
128
|
+
throw new AgentEditNotSupportedError('model in this file is not a plain inline object — it may use a custom createModelCall; edit it directly.');
|
|
129
|
+
}
|
|
130
|
+
const provider = fields.model.provider;
|
|
131
|
+
const modelName = fields.model.model?.trim() || (provider === 'anthropic' ? 'claude-sonnet-5' : '');
|
|
132
|
+
if (provider !== 'anthropic' && !modelName) {
|
|
133
|
+
throw new AgentModelError(`A model name is required for provider '${provider}' — only anthropic has a default (claude-sonnet-5).`);
|
|
134
|
+
}
|
|
135
|
+
edits.push({
|
|
136
|
+
start: prop.initializer.getStart(sourceFile),
|
|
137
|
+
end: prop.initializer.getEnd(),
|
|
138
|
+
text: `{ provider: '${provider}', model: ${tsStringLiteral(modelName)} }`,
|
|
139
|
+
});
|
|
140
|
+
result.model = { provider, model: modelName };
|
|
141
|
+
// The property's own value is now correct, but a stale
|
|
142
|
+
// "// reads ANTHROPIC_API_KEY" sitting right after it (from before a
|
|
143
|
+
// provider change) would be actively misleading, not just cosmetic —
|
|
144
|
+
// it'd point an operator at the wrong env var. Only touched if it
|
|
145
|
+
// still matches the exact shape agentIndexTemplate itself generates
|
|
146
|
+
// (see MODEL_COMMENT_PATTERN) — a hand-written comment in that spot
|
|
147
|
+
// is left alone, same "don't guess" rule the value itself follows.
|
|
148
|
+
const comment = trailingCommentAfter(source, prop.getEnd());
|
|
149
|
+
if (comment && MODEL_COMMENT_PATTERN.test(source.slice(comment.pos, comment.end))) {
|
|
150
|
+
edits.push({ start: comment.pos, end: comment.end, text: `// reads ${MODEL_ENV_VAR[provider]}` });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!edits.length)
|
|
154
|
+
return result;
|
|
155
|
+
// Apply from the end of the file backward — each edit's start/end was
|
|
156
|
+
// computed against the *original* source, so splicing front-to-back
|
|
157
|
+
// would shift every later offset out from under the next edit.
|
|
158
|
+
edits.sort((a, b) => b.start - a.start);
|
|
159
|
+
let updated = source;
|
|
160
|
+
for (const edit of edits) {
|
|
161
|
+
updated = updated.slice(0, edit.start) + edit.text + updated.slice(edit.end);
|
|
162
|
+
}
|
|
163
|
+
writeFileSync(indexPath, updated);
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const agentsConfigPageHtml: string;
|