chore: consolidate admin cleanup and colima config
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
/**
|
||||
* GoosedExecutor — runs a skill via a one-shot goosed session.
|
||||
* GoosedExecutor — runs a skill via a goosed session.
|
||||
*
|
||||
* Session lifecycle:
|
||||
* 1. POST /agent/start { messages: [{ role: 'user', content: prompt }] }
|
||||
* 2. GET /sessions/:id/events (SSE) → collect until Finish event or timeout
|
||||
* 3. DELETE /sessions/:id (best-effort cleanup)
|
||||
* 1. POST /agent/start { working_dir }
|
||||
* 2. POST /sessions/:id/reply { request_id: UUID, user_message: {...} }
|
||||
* 3. GET /sessions/:id/events (SSE) → collect until Finish event or timeout
|
||||
* 4. DELETE /sessions/:id (best-effort cleanup)
|
||||
*
|
||||
* Dual-node: pickGoosedUrl() round-robins across configured nodes.
|
||||
* Protocol: HTTPS with self-signed cert (rejectUnauthorized: false).
|
||||
*/
|
||||
|
||||
import https from 'node:https';
|
||||
import crypto from 'node:crypto';
|
||||
import { goosedFetch, pickGoosedUrl } from '../goosed-client.mjs';
|
||||
|
||||
const SESSION_TIMEOUT_MS = Number(process.env.SKILL_GOOSED_TIMEOUT_MS ?? 120_000);
|
||||
@@ -18,19 +20,14 @@ const tlsAgent = new https.Agent({ rejectUnauthorized: false });
|
||||
|
||||
export async function invokeGoosedSkill(skill, input) {
|
||||
const node = pickGoosedUrl();
|
||||
const prompt = buildPrompt(skill, input);
|
||||
|
||||
const systemPrompt = buildSystemPrompt(skill, input);
|
||||
const userPrompt = buildUserPrompt(skill, input);
|
||||
|
||||
// 1. Start session
|
||||
// 1. Start session (no messages yet)
|
||||
const startRes = await goosedFetch('/agent/start', {
|
||||
method: 'POST',
|
||||
node,
|
||||
body: {
|
||||
version: '1.0.0',
|
||||
session_id: null,
|
||||
...(systemPrompt ? { system_prompt_override: systemPrompt } : {}),
|
||||
messages: [{ role: 'user', content: userPrompt }],
|
||||
working_dir: process.env.GOOSE_SKILL_WORKING_DIR ?? '/tmp',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,12 +38,42 @@ export async function invokeGoosedSkill(skill, input) {
|
||||
);
|
||||
}
|
||||
|
||||
const sessionId = startRes.body?.session_id ?? startRes.body?.id;
|
||||
const sessionId = startRes.body?.id ?? startRes.body?.session_id;
|
||||
if (!sessionId) {
|
||||
throw new Error(`goosed did not return a session_id: ${JSON.stringify(startRes.body)}`);
|
||||
throw new Error(`goosed did not return a session id: ${JSON.stringify(startRes.body)}`);
|
||||
}
|
||||
|
||||
// 2. Stream events and collect result
|
||||
// 1b. Set the LLM provider on the session
|
||||
const goosedProvider = process.env.GOOSE_PROVIDER ?? process.env.GOOSED_PROVIDER ?? 'custom_deepseek';
|
||||
const goosedModel = process.env.GOOSE_MODEL ?? process.env.GOOSED_MODEL;
|
||||
const providerBody = { session_id: sessionId, provider: goosedProvider };
|
||||
if (goosedModel) providerBody.model = goosedModel;
|
||||
await goosedFetch('/agent/update_provider', { method: 'POST', node, body: providerBody });
|
||||
|
||||
// 2. Send the prompt via reply
|
||||
const requestId = crypto.randomUUID();
|
||||
const replyRes = await goosedFetch(`/sessions/${sessionId}/reply`, {
|
||||
method: 'POST',
|
||||
node,
|
||||
body: {
|
||||
request_id: requestId,
|
||||
user_message: {
|
||||
role: 'user',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
metadata: { userVisible: true, agentVisible: true },
|
||||
content: [{ type: 'text', text: prompt }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!replyRes.ok) {
|
||||
throw Object.assign(
|
||||
new Error(`goosed /sessions/${sessionId}/reply failed (${replyRes.status})`),
|
||||
{ status: replyRes.status, upstream: replyRes.body },
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Stream SSE events and collect result
|
||||
try {
|
||||
return await collectSessionResult(node, sessionId);
|
||||
} finally {
|
||||
@@ -54,14 +81,22 @@ export async function invokeGoosedSkill(skill, input) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildSystemPrompt(skill, input) {
|
||||
if (skill.runtime?.config?.system_prompt) return skill.runtime.config.system_prompt;
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildUserPrompt(skill, input) {
|
||||
function buildPrompt(skill, input) {
|
||||
if (input.design_md_content) {
|
||||
return `You are a UI generation assistant. Follow this design system exactly:\n\n${input.design_md_content}\n\nTask: ${input.task ?? input.prompt ?? ''}`;
|
||||
return `You are a UI generation assistant. Your output must be a complete, self-contained HTML page — not a component, not a snippet, not a code block with backticks. Output raw HTML only, starting with <!DOCTYPE html>.
|
||||
|
||||
Apply the following design system exactly. Match the colors, typography, spacing, border styles, shadows, and interaction patterns described:
|
||||
|
||||
${input.design_md_content}
|
||||
|
||||
Task: ${input.task ?? input.prompt ?? ''}
|
||||
|
||||
Requirements:
|
||||
- Output a single complete HTML file with <html>, <head>, <body> and all inline <style> — no external stylesheets or JS frameworks
|
||||
- The page should feel like a real product page from this brand, not a generic template
|
||||
- Use the exact color values, font sizes, border-radius, shadow formulas from the design system above
|
||||
- Include realistic placeholder content relevant to the task
|
||||
- The page must be visually complete and ready to render in a browser`;
|
||||
}
|
||||
return input.task ?? input.prompt ?? JSON.stringify(input);
|
||||
}
|
||||
@@ -80,13 +115,10 @@ function collectSessionResult(node, sessionId) {
|
||||
path: url.pathname,
|
||||
method: 'GET',
|
||||
headers: { 'X-Secret-Key': secret, Accept: 'text/event-stream' },
|
||||
agent: url.protocol === 'https:' ? tlsAgent : undefined,
|
||||
rejectUnauthorized: false,
|
||||
agent: tlsAgent,
|
||||
};
|
||||
|
||||
const proto = url.protocol === 'https:' ? https : (() => { throw new Error('http not supported for goosed'); })();
|
||||
|
||||
const req = proto.request(options, (res) => {
|
||||
const req = https.request(options, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
res.resume();
|
||||
return reject(new Error(`SSE stream returned ${res.statusCode}`));
|
||||
@@ -144,14 +176,12 @@ function collectSessionResult(node, sessionId) {
|
||||
}
|
||||
|
||||
function extractText(event) {
|
||||
// Handle goosed SSE event shapes
|
||||
if (event?.type === 'Message' && event?.message?.role === 'assistant') {
|
||||
return (event.message.content ?? [])
|
||||
.filter((c) => c.type === 'text')
|
||||
.map((c) => c.text)
|
||||
.join('');
|
||||
}
|
||||
// Simpler delta format
|
||||
if (event?.delta?.text) return event.delta.text;
|
||||
if (typeof event?.content === 'string') return event.content;
|
||||
return '';
|
||||
|
||||
@@ -46,28 +46,46 @@ export function goosedUrls() {
|
||||
/**
|
||||
* Make a JSON request to a goosed node.
|
||||
* Returns { ok, status, body } — never throws on HTTP errors, only on network failure.
|
||||
* Uses https.request directly so rejectUnauthorized: false works for self-signed certs.
|
||||
*/
|
||||
export async function goosedFetch(path, { method = 'GET', body, node } = {}) {
|
||||
export function goosedFetch(path, { method = 'GET', body, node } = {}) {
|
||||
const base = node ?? pickGoosedUrl();
|
||||
const secret = process.env.GOOSE_SERVER__SECRET_KEY ??
|
||||
const secret =
|
||||
process.env.GOOSE_SERVER__SECRET_KEY ??
|
||||
process.env.TKMIND_SERVER__SECRET_KEY ??
|
||||
'local-dev-secret';
|
||||
|
||||
const url = `${base}${path}`;
|
||||
const init = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': secret,
|
||||
},
|
||||
agent: tlsAgent,
|
||||
};
|
||||
if (body !== undefined) init.body = JSON.stringify(body);
|
||||
const url = new URL(`${base}${path}`);
|
||||
const payload = body !== undefined ? JSON.stringify(body) : null;
|
||||
|
||||
const res = await fetch(url, init);
|
||||
let json = null;
|
||||
const text = await res.text();
|
||||
try { json = JSON.parse(text); } catch { json = { raw: text }; }
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Secret-Key': secret,
|
||||
...(payload ? { 'Content-Length': Buffer.byteLength(payload) } : {}),
|
||||
},
|
||||
agent: tlsAgent,
|
||||
};
|
||||
|
||||
return { ok: res.ok, status: res.status, body: json };
|
||||
const req = https.request(options, (res) => {
|
||||
let text = '';
|
||||
res.setEncoding('utf8');
|
||||
res.on('data', (chunk) => { text += chunk; });
|
||||
res.on('end', () => {
|
||||
let json = null;
|
||||
try { json = JSON.parse(text); } catch { json = { raw: text }; }
|
||||
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, body: json });
|
||||
});
|
||||
res.on('error', reject);
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (payload) req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function ensureSkillSchema(pool) {
|
||||
CREATE TABLE IF NOT EXISTS adm_skill_invocations (
|
||||
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
skill_id INT UNSIGNED NOT NULL,
|
||||
caller_id INT UNSIGNED NULL,
|
||||
caller_id VARCHAR(64) NULL,
|
||||
caller_type VARCHAR(32) NOT NULL DEFAULT 'unknown',
|
||||
input_snapshot JSON NULL,
|
||||
status ENUM('pending','running','success','failed','blocked_by_flag')
|
||||
|
||||
Reference in New Issue
Block a user