pgkiln 0.78.0 → 0.80.0
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/db/migrations/144_workflow_migrate_branches.sql +186 -0
- package/db/migrations/145_remote_databases.sql +41 -0
- package/docs/apex-feature-parity.md +7 -7
- package/docs/guide/03-builder.md +37 -0
- package/docs/guide/04-pages-and-regions.md +8 -2
- package/docs/guide/06-processing.md +13 -2
- package/docs/guide/15-extensions.md +1 -1
- package/docs/guide/18-cli.md +19 -2
- package/docs/guide/19-rest-data-sources.md +2 -1
- package/docs/guide/20-ai-agents.md +1 -1
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/app.js +83 -0
- package/src/ai/chat.ts +32 -5
- package/src/app.ts +2 -0
- package/src/builder/advisor.ts +305 -149
- package/src/builder/home.ts +1 -0
- package/src/builder/remote-databases.ts +215 -0
- package/src/builder/search.ts +15 -4
- package/src/builder/workflow-migrate.ts +19 -13
- package/src/cli/advise.ts +39 -15
- package/src/cli/main.ts +7 -4
- package/src/cli/mcp.ts +10 -2
- package/src/json-sources.ts +14 -8
- package/src/remote-db.ts +145 -0
- package/src/runtime/assistant.ts +54 -17
package/public/app.js
CHANGED
|
@@ -4041,6 +4041,89 @@ function areaFilter(map, f) {
|
|
|
4041
4041
|
btn.addEventListener('click', (x) => x.preventDefault());
|
|
4042
4042
|
}
|
|
4043
4043
|
});
|
|
4044
|
+
// (sprint 148, WS-4) the answer as the model writes it: the message is posted with fetch asking for server-sent
|
|
4045
|
+
// events; "text" pieces are added as text (never as HTML), "tool" names the tool being used, and "done" loads the
|
|
4046
|
+
// page, which shows the saved answer formatted, its links and actions, or the error. Without fetch streams (or when
|
|
4047
|
+
// the server answers with a page, e.g. a refusal) the form is sent the ordinary way.
|
|
4048
|
+
const streams = typeof ReadableStream !== 'undefined' && typeof TextDecoder !== 'undefined' && typeof fetch === 'function';
|
|
4049
|
+
const go = (url) => {
|
|
4050
|
+
const u = new URL(url, location.href);
|
|
4051
|
+
if (u.pathname === location.pathname && u.search === location.search) {
|
|
4052
|
+
history.replaceState(history.state, '', u);
|
|
4053
|
+
location.reload();
|
|
4054
|
+
} else location.assign(u);
|
|
4055
|
+
};
|
|
4056
|
+
async function streamAnswer(f) {
|
|
4057
|
+
const box = document.querySelector(`textarea[form="${CSS.escape(f.id)}"]`);
|
|
4058
|
+
const wrap = box?.closest('.assistant');
|
|
4059
|
+
const log = wrap?.querySelector('.assistant-log');
|
|
4060
|
+
const entry = (role, label) => {
|
|
4061
|
+
const li = document.createElement('li');
|
|
4062
|
+
li.className = `assistant-msg assistant-msg-${role}`;
|
|
4063
|
+
const who = document.createElement('span');
|
|
4064
|
+
who.className = 'assistant-who';
|
|
4065
|
+
who.textContent = label || '';
|
|
4066
|
+
const text = document.createElement('div');
|
|
4067
|
+
text.className = 'assistant-text';
|
|
4068
|
+
li.append(who, text);
|
|
4069
|
+
log.append(li);
|
|
4070
|
+
return { li, text };
|
|
4071
|
+
};
|
|
4072
|
+
const mine = entry('user', wrap.dataset.you);
|
|
4073
|
+
const said = document.createElement('p');
|
|
4074
|
+
said.textContent = box.value;
|
|
4075
|
+
mine.text.append(said);
|
|
4076
|
+
const answer = entry('assistant', wrap.dataset.who);
|
|
4077
|
+
answer.text.setAttribute('aria-busy', 'true');
|
|
4078
|
+
const para = document.createElement('p');
|
|
4079
|
+
para.className = 'assistant-streaming';
|
|
4080
|
+
answer.text.append(para);
|
|
4081
|
+
const status = document.createElement('small');
|
|
4082
|
+
status.className = 'assistant-tools muted';
|
|
4083
|
+
answer.li.append(status);
|
|
4084
|
+
answer.li.scrollIntoView({ block: 'nearest' });
|
|
4085
|
+
let res;
|
|
4086
|
+
try {
|
|
4087
|
+
res = await fetch(f.action, { method: 'POST', body: new URLSearchParams(new FormData(f)), headers: { accept: 'text/event-stream' }, credentials: 'same-origin' });
|
|
4088
|
+
} catch {
|
|
4089
|
+
return HTMLFormElement.prototype.submit.call(f);
|
|
4090
|
+
}
|
|
4091
|
+
if (!res.ok || !(res.headers.get('content-type') || '').startsWith('text/event-stream') || !res.body) return HTMLFormElement.prototype.submit.call(f);
|
|
4092
|
+
const reader = res.body.getReader();
|
|
4093
|
+
const decoder = new TextDecoder();
|
|
4094
|
+
let buf = '';
|
|
4095
|
+
try {
|
|
4096
|
+
for (;;) {
|
|
4097
|
+
const { value, done } = await reader.read();
|
|
4098
|
+
if (done) break;
|
|
4099
|
+
buf += decoder.decode(value, { stream: true });
|
|
4100
|
+
let cut;
|
|
4101
|
+
while ((cut = buf.indexOf('\n\n')) >= 0) {
|
|
4102
|
+
const block = buf.slice(0, cut);
|
|
4103
|
+
buf = buf.slice(cut + 2);
|
|
4104
|
+
const event = /^event: (.*)$/m.exec(block)?.[1];
|
|
4105
|
+
const line = /^data: (.*)$/m.exec(block)?.[1];
|
|
4106
|
+
if (!event || line === undefined) continue;
|
|
4107
|
+
const data = JSON.parse(line);
|
|
4108
|
+
if (event === 'text' && typeof data === 'string') {
|
|
4109
|
+
para.textContent += data;
|
|
4110
|
+
answer.li.scrollIntoView({ block: 'nearest' });
|
|
4111
|
+
} else if (event === 'tool' && typeof data === 'string') status.textContent = (wrap.dataset.used || '{tools}').replace('{tools}', data);
|
|
4112
|
+
else if (event === 'done' && data && typeof data.url === 'string') return go(data.url);
|
|
4113
|
+
}
|
|
4114
|
+
}
|
|
4115
|
+
} catch {
|
|
4116
|
+
// the connection broke: the server still saves the answer; the page shows it
|
|
4117
|
+
}
|
|
4118
|
+
location.reload();
|
|
4119
|
+
}
|
|
4120
|
+
document.addEventListener('submit', (e) => {
|
|
4121
|
+
const f = e.target;
|
|
4122
|
+
if (e.defaultPrevented || !streams || !f.matches || !f.matches('form.assistant-form') || !f.id) return;
|
|
4123
|
+
if (!document.querySelector(`textarea[form="${CSS.escape(f.id)}"]`)?.closest('.assistant')?.querySelector('.assistant-log')) return;
|
|
4124
|
+
e.preventDefault();
|
|
4125
|
+
streamAnswer(f);
|
|
4126
|
+
});
|
|
4044
4127
|
// Ctrl+Enter (or ⌘+Enter) sends the message
|
|
4045
4128
|
document.addEventListener('keydown', (e) => {
|
|
4046
4129
|
const box = e.target;
|
package/src/ai/chat.ts
CHANGED
|
@@ -52,6 +52,12 @@ export interface ChatRequest {
|
|
|
52
52
|
effort?: 'low' | 'medium' | 'high' | null;
|
|
53
53
|
/** (121) an agent's output limit per call (never above the service's maximum) */
|
|
54
54
|
maxTokens?: number | null;
|
|
55
|
+
/**
|
|
56
|
+
* (sprint 148, WS-4) streamed answers: the text as the model writes it, in pieces (also the text of a round that
|
|
57
|
+
* then calls tools), and each tool before it runs. Without them a call waits for the whole answer, as before.
|
|
58
|
+
*/
|
|
59
|
+
onText?: (delta: string) => void;
|
|
60
|
+
onTool?: (name: string) => void;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export interface ChatResult {
|
|
@@ -122,7 +128,7 @@ async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], r
|
|
|
122
128
|
const msg = await metered(s, limits, info, async () => {
|
|
123
129
|
let m: Anthropic.Beta.BetaMessage;
|
|
124
130
|
try {
|
|
125
|
-
|
|
131
|
+
const stream = client.beta.messages.stream({
|
|
126
132
|
model: s.model,
|
|
127
133
|
max_tokens: outputLimit(s, req),
|
|
128
134
|
system: req.system,
|
|
@@ -130,7 +136,9 @@ async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], r
|
|
|
130
136
|
...(tools.length ? { tools } : {}),
|
|
131
137
|
...((req.effort ?? s.effort) ? { output_config: { effort: (req.effort ?? s.effort)! } } : {}),
|
|
132
138
|
...(s.refusal_fallback ? { betas: ['server-side-fallback-2026-07-01'], fallbacks: 'default' as const } : {}),
|
|
133
|
-
})
|
|
139
|
+
});
|
|
140
|
+
if (req.onText) stream.on('text', (delta) => req.onText!(delta));
|
|
141
|
+
m = await stream.finalMessage();
|
|
134
142
|
} catch (e) {
|
|
135
143
|
throw claudeError(e);
|
|
136
144
|
}
|
|
@@ -150,6 +158,7 @@ async function claudeChat(s: AiService, limits: Parameters<typeof metered>[1], r
|
|
|
150
158
|
if (round >= req.maxRounds) throw new AiError('output', TOO_MANY_ROUNDS);
|
|
151
159
|
const results: Anthropic.Beta.BetaToolResultBlockParam[] = [];
|
|
152
160
|
for (const call of calls) {
|
|
161
|
+
req.onTool?.(call.name);
|
|
153
162
|
const out = await req.runTool(call.name, (call.input ?? {}) as Record<string, unknown>);
|
|
154
163
|
used.push({ name: call.name, ok: !out.isError });
|
|
155
164
|
results.push({ type: 'tool_result', tool_use_id: call.id, content: clip(out.content), ...(out.isError ? { is_error: true } : {}) });
|
|
@@ -169,15 +178,32 @@ async function openAiChat(s: AiService, limits: Parameters<typeof metered>[1], r
|
|
|
169
178
|
const res = await metered(s, limits, info, async () => {
|
|
170
179
|
let r: OpenAI.Chat.ChatCompletion;
|
|
171
180
|
try {
|
|
172
|
-
|
|
181
|
+
const params = {
|
|
173
182
|
model: s.model,
|
|
174
183
|
// the system prompt is sent with each request, not kept in the history
|
|
175
|
-
messages: [{ role: 'system', content: req.system }, ...messages],
|
|
184
|
+
messages: [{ role: 'system' as const, content: req.system }, ...messages],
|
|
176
185
|
max_completion_tokens: outputLimit(s, req),
|
|
177
186
|
...(tools.length ? { tools } : {}),
|
|
178
187
|
// (121) an agent's reasoning effort; models without reasoning refuse it, so none is sent without one
|
|
179
188
|
...(req.effort ? { reasoning_effort: req.effort } : {}),
|
|
180
|
-
}
|
|
189
|
+
};
|
|
190
|
+
if (req.onText) {
|
|
191
|
+
// (sprint 148) streamed: the content as it comes; the usage in the last chunk
|
|
192
|
+
// an OpenAI-compatible endpoint that refuses streaming (400) gets the plain request once
|
|
193
|
+
let streamed: OpenAI.Chat.ChatCompletion | null = null;
|
|
194
|
+
let sent = false;
|
|
195
|
+
try {
|
|
196
|
+
const stream = client.chat.completions.stream({ ...params, stream_options: { include_usage: true } });
|
|
197
|
+
stream.on('content', (delta) => {
|
|
198
|
+
sent = true;
|
|
199
|
+
req.onText!(delta);
|
|
200
|
+
});
|
|
201
|
+
streamed = await stream.finalChatCompletion();
|
|
202
|
+
} catch (e) {
|
|
203
|
+
if (sent || (e as { status?: number }).status !== 400) throw e;
|
|
204
|
+
}
|
|
205
|
+
r = streamed ?? (await client.chat.completions.create(params));
|
|
206
|
+
} else r = await client.chat.completions.create(params);
|
|
181
207
|
} catch (e) {
|
|
182
208
|
throw openAiError(e);
|
|
183
209
|
}
|
|
@@ -198,6 +224,7 @@ async function openAiChat(s: AiService, limits: Parameters<typeof metered>[1], r
|
|
|
198
224
|
for (const call of calls) {
|
|
199
225
|
let input: Record<string, unknown>;
|
|
200
226
|
let out: ToolOutcome;
|
|
227
|
+
req.onTool?.(call.function.name);
|
|
201
228
|
try {
|
|
202
229
|
input = JSON.parse(call.function.arguments || '{}');
|
|
203
230
|
out = input && typeof input === 'object' && !Array.isArray(input) ? await req.runTool(call.function.name, input) : { content: 'The arguments are not a JSON object.', isError: true };
|
package/src/app.ts
CHANGED
|
@@ -99,6 +99,7 @@ import { mailQueueRoutes } from './builder/mail-queue.ts';
|
|
|
99
99
|
import { networkAccessRoutes } from './builder/network-access.ts';
|
|
100
100
|
import { workspaceMailRoutes } from './builder/workspace-mail.ts';
|
|
101
101
|
import { workspaceResourceRoutes } from './builder/workspace-resources.ts';
|
|
102
|
+
import { remoteDatabaseRoutes } from './builder/remote-databases.ts';
|
|
102
103
|
import { runtimeScope } from './db.ts';
|
|
103
104
|
// (sprint 139, AP-6) OpenTelemetry: a trace per request, /metrics, the browser relay
|
|
104
105
|
import { startTelemetry } from './telemetry/export.ts';
|
|
@@ -288,6 +289,7 @@ export async function buildApp(opts: { logger?: boolean } = {}) {
|
|
|
288
289
|
await app.register(networkAccessRoutes);
|
|
289
290
|
await app.register(workspaceMailRoutes);
|
|
290
291
|
await app.register(workspaceResourceRoutes);
|
|
292
|
+
await app.register(remoteDatabaseRoutes);
|
|
291
293
|
// (sprint 138, AP-5) an application's Utilities
|
|
292
294
|
await app.register(utilityRoutes);
|
|
293
295
|
await app.register(dependencyRoutes);
|