@d4y/agent-runtime-nuxt 0.1.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/README.md +314 -0
- package/dist/module.d.mts +69 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +60 -0
- package/dist/runtime/components/AgentRuntimeArtifactPreview.d.vue.ts +14 -0
- package/dist/runtime/components/AgentRuntimeArtifactPreview.vue +112 -0
- package/dist/runtime/components/AgentRuntimeArtifactPreview.vue.d.ts +14 -0
- package/dist/runtime/composables/useAgentRuntime.d.ts +130 -0
- package/dist/runtime/composables/useAgentRuntime.js +306 -0
- package/dist/runtime/composables/useAgentRuntimeMarkdown.d.ts +15 -0
- package/dist/runtime/composables/useAgentRuntimeMarkdown.js +109 -0
- package/dist/runtime/server/api/app.get.d.ts +6 -0
- package/dist/runtime/server/api/app.get.js +26 -0
- package/dist/runtime/server/api/conversations/[id]/abort.post.d.ts +6 -0
- package/dist/runtime/server/api/conversations/[id]/abort.post.js +18 -0
- package/dist/runtime/server/api/conversations/[id]/env.post.d.ts +10 -0
- package/dist/runtime/server/api/conversations/[id]/env.post.js +22 -0
- package/dist/runtime/server/api/conversations/[id]/files/raw/[...path].get.d.ts +11 -0
- package/dist/runtime/server/api/conversations/[id]/files/raw/[...path].get.js +31 -0
- package/dist/runtime/server/api/conversations/[id]/files.get.d.ts +14 -0
- package/dist/runtime/server/api/conversations/[id]/files.get.js +16 -0
- package/dist/runtime/server/api/conversations/[id]/history.get.d.ts +2 -0
- package/dist/runtime/server/api/conversations/[id]/history.get.js +16 -0
- package/dist/runtime/server/api/conversations/[id]/messages.post.d.ts +7 -0
- package/dist/runtime/server/api/conversations/[id]/messages.post.js +20 -0
- package/dist/runtime/server/api/conversations/[id]/stream.get.d.ts +9 -0
- package/dist/runtime/server/api/conversations/[id]/stream.get.js +28 -0
- package/dist/runtime/server/api/conversations/[id].delete.d.ts +2 -0
- package/dist/runtime/server/api/conversations/[id].delete.js +18 -0
- package/dist/runtime/server/api/conversations.post.d.ts +7 -0
- package/dist/runtime/server/api/conversations.post.js +17 -0
- package/dist/runtime/server/utils/agent-runtime.d.ts +12 -0
- package/dist/runtime/server/utils/agent-runtime.js +12 -0
- package/dist/runtime/utils/files.d.ts +16 -0
- package/dist/runtime/utils/files.js +42 -0
- package/dist/types.d.mts +9 -0
- package/package.json +67 -0
- package/src/frontend.ts +16 -0
- package/src/module.ts +155 -0
- package/src/nitro-globals.d.ts +8 -0
- package/src/runtime/components/AgentRuntimeArtifactPreview.vue +192 -0
- package/src/runtime/composables/useAgentRuntime.ts +527 -0
- package/src/runtime/composables/useAgentRuntimeMarkdown.ts +145 -0
- package/src/runtime/server/api/app.get.ts +50 -0
- package/src/runtime/server/api/conversations/[id]/abort.post.ts +26 -0
- package/src/runtime/server/api/conversations/[id]/env.post.ts +34 -0
- package/src/runtime/server/api/conversations/[id]/files/raw/[...path].get.ts +48 -0
- package/src/runtime/server/api/conversations/[id]/files.get.ts +33 -0
- package/src/runtime/server/api/conversations/[id]/history.get.ts +20 -0
- package/src/runtime/server/api/conversations/[id]/messages.post.ts +29 -0
- package/src/runtime/server/api/conversations/[id]/stream.get.ts +41 -0
- package/src/runtime/server/api/conversations/[id].delete.ts +22 -0
- package/src/runtime/server/api/conversations.post.ts +26 -0
- package/src/runtime/server/utils/agent-runtime.ts +33 -0
- package/src/runtime/utils/files.ts +78 -0
- package/src/shared.ts +46 -0
- package/src/vue-shim.d.ts +6 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { type Ref, type ComputedRef } from 'vue';
|
|
2
|
+
import type { UIMessage as AiUIMessage } from 'ai';
|
|
3
|
+
/**
|
|
4
|
+
* Re-export the AI SDK's `UIMessage` shape — this is the type Nuxt UI's chat
|
|
5
|
+
* components accept, so we use it everywhere in the public surface to keep
|
|
6
|
+
* downstream code zero-friction.
|
|
7
|
+
*/
|
|
8
|
+
export type UIMessage = AiUIMessage;
|
|
9
|
+
/**
|
|
10
|
+
* Lifecycle status for a chat. Mirrors what the AI-SDK `UIMessage` consumers
|
|
11
|
+
* expect, so you can wire this directly into Nuxt UI's `<UChatMessages :status>`.
|
|
12
|
+
*/
|
|
13
|
+
export type ChatStatus = 'idle' | 'submitted' | 'streaming' | 'ready' | 'error';
|
|
14
|
+
/** Single env-var slot declared by an app's manifest. */
|
|
15
|
+
export interface AppEnvField {
|
|
16
|
+
required: boolean;
|
|
17
|
+
secret: boolean;
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
/** Manifest summary for the active app, as returned by `${apiPrefix}/app`. */
|
|
21
|
+
export interface AppInfo {
|
|
22
|
+
appId: string;
|
|
23
|
+
name: string;
|
|
24
|
+
envSchema: Record<string, AppEnvField>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Generic auth state. Keys are app-defined (whatever the active app declares
|
|
28
|
+
* in its manifest `envSchema`) — the module does not care what they mean,
|
|
29
|
+
* it just ships the resulting map to agent-runtime as the conversation env.
|
|
30
|
+
*/
|
|
31
|
+
export interface AppAuth {
|
|
32
|
+
values: Record<string, string>;
|
|
33
|
+
}
|
|
34
|
+
/** Side-channel UI-action event emitted by the agent (via the `ui_action` SSE event). */
|
|
35
|
+
export interface UiAction {
|
|
36
|
+
toolCallId: string;
|
|
37
|
+
type: string;
|
|
38
|
+
payload: unknown;
|
|
39
|
+
at: number;
|
|
40
|
+
}
|
|
41
|
+
/** Workspace file entry, mirrors the backend response. */
|
|
42
|
+
export interface FileEntry {
|
|
43
|
+
name: string;
|
|
44
|
+
relPath: string;
|
|
45
|
+
size: number;
|
|
46
|
+
mtime: string;
|
|
47
|
+
mimeType: string;
|
|
48
|
+
}
|
|
49
|
+
/** Per-turn options for `send`. */
|
|
50
|
+
export interface SendOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Optional dynamic context object forwarded verbatim to the agent for this
|
|
53
|
+
* turn (the harness passes it into `agent.run`'s `context` field).
|
|
54
|
+
*/
|
|
55
|
+
context?: unknown;
|
|
56
|
+
/**
|
|
57
|
+
* Pre-`send` hook to mutate the *forwarded* prompt without changing what is
|
|
58
|
+
* displayed to the user. Useful for model-specific soft switches like
|
|
59
|
+
* Qwen3's `/think` / `/no_think` toggles. Return the rewritten string.
|
|
60
|
+
*/
|
|
61
|
+
rewriteContent?: (text: string) => string;
|
|
62
|
+
/** Optional language override for this turn / conversation, e.g. `en` or `de-CH`. */
|
|
63
|
+
language?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Options for `start`. */
|
|
66
|
+
export interface StartOptions {
|
|
67
|
+
/** Optional language override for the newly created conversation. */
|
|
68
|
+
language?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Return shape of {@link useAgentRuntime}. All members are reactive. */
|
|
71
|
+
export interface UseAgentRuntime {
|
|
72
|
+
/** Active conversation id, or `null` until `start()` (or the first `send()`) succeeds. */
|
|
73
|
+
conversationId: Ref<string | null>;
|
|
74
|
+
/** Ordered list of UI messages to render. Mutated as SSE events arrive. */
|
|
75
|
+
messages: Ref<UIMessage[]>;
|
|
76
|
+
/** Lifecycle status — drive your UI from this. */
|
|
77
|
+
status: Ref<ChatStatus>;
|
|
78
|
+
/** Last error encountered (network failure, upstream error, missing auth, ...). */
|
|
79
|
+
error: Ref<Error | null>;
|
|
80
|
+
/** Most recent UI-action events, newest first, capped at 50. */
|
|
81
|
+
uiActions: Ref<UiAction[]>;
|
|
82
|
+
/** Active app manifest. `null` until the first `onMounted` fetch resolves. */
|
|
83
|
+
app: Ref<AppInfo | null>;
|
|
84
|
+
/** Current auth values (persisted in localStorage, scoped per appId). */
|
|
85
|
+
auth: Ref<AppAuth>;
|
|
86
|
+
/** True iff every required env var has a non-empty value. */
|
|
87
|
+
authReady: ComputedRef<boolean>;
|
|
88
|
+
/** Workspace files for the current conversation. Refreshed on every stream `end` event. */
|
|
89
|
+
files: Ref<FileEntry[]>;
|
|
90
|
+
/** Build the proxied download URL for a workspace-relative path. */
|
|
91
|
+
fileUrl: (relPath: string) => string;
|
|
92
|
+
/** Manually re-fetch the workspace file listing. */
|
|
93
|
+
refreshFiles: () => Promise<void>;
|
|
94
|
+
/** Persist a new auth map; if a conversation is open, push the change to its env. */
|
|
95
|
+
saveAuth: (next: AppAuth) => Promise<void>;
|
|
96
|
+
/** Open a fresh conversation with the current auth. Returns the new id. */
|
|
97
|
+
start: (options?: StartOptions) => Promise<string>;
|
|
98
|
+
/** Append a user message; lazily calls `start()` if no conversation exists yet. */
|
|
99
|
+
send: (text: string, options?: SendOptions) => Promise<void>;
|
|
100
|
+
/** Cancel the in-flight agent run. */
|
|
101
|
+
abort: () => Promise<void>;
|
|
102
|
+
/** Tear down state — drops the conversation reference, clears messages and files. */
|
|
103
|
+
reset: () => Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Headless chat client for agent-runtime.
|
|
107
|
+
*
|
|
108
|
+
* Wires up:
|
|
109
|
+
* - Conversation lifecycle (`start`, `send`, `abort`, `reset`).
|
|
110
|
+
* - SSE stream consumption with incremental message updates.
|
|
111
|
+
* - Generic auth/env state (whatever the active app declares).
|
|
112
|
+
* - Workspace file listing + a download URL builder.
|
|
113
|
+
*
|
|
114
|
+
* The composable is **headless** — it produces reactive state shaped for
|
|
115
|
+
* AI-SDK / Nuxt UI chat components but never imports a single UI package
|
|
116
|
+
* itself. Render whatever you want on top.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```vue
|
|
120
|
+
* <script setup lang="ts">
|
|
121
|
+
* const chat = useAgentRuntime()
|
|
122
|
+
* </script>
|
|
123
|
+
*
|
|
124
|
+
* <template>
|
|
125
|
+
* <UChatMessages :messages="chat.messages.value" :status="chat.status.value" />
|
|
126
|
+
* <UChatPrompt v-model="input" @submit="chat.send(input)" />
|
|
127
|
+
* </template>
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export declare const useAgentRuntime: () => UseAgentRuntime;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { computed, onMounted, ref, shallowRef } from "vue";
|
|
2
|
+
import { useRuntimeConfig } from "nuxt/app";
|
|
3
|
+
const newId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `id-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
4
|
+
const authStorageKey = (appId) => `agent-runtime.app-auth.${appId}`;
|
|
5
|
+
const emptyAuth = () => ({ values: {} });
|
|
6
|
+
const loadAuth = (appId) => {
|
|
7
|
+
if (typeof window === "undefined") return emptyAuth();
|
|
8
|
+
try {
|
|
9
|
+
const raw = window.localStorage.getItem(authStorageKey(appId));
|
|
10
|
+
if (!raw) return emptyAuth();
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
return { values: { ...parsed.values ?? {} } };
|
|
13
|
+
} catch {
|
|
14
|
+
return emptyAuth();
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const persistAuth = (appId, auth) => {
|
|
18
|
+
if (typeof window === "undefined") return;
|
|
19
|
+
try {
|
|
20
|
+
window.localStorage.setItem(authStorageKey(appId), JSON.stringify(auth));
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const buildEnvFromAuth = (auth) => {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [k, v] of Object.entries(auth.values)) {
|
|
27
|
+
const trimmed = (v ?? "").trim();
|
|
28
|
+
if (trimmed) out[k] = trimmed;
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
};
|
|
32
|
+
const computeAuthReady = (auth, app) => {
|
|
33
|
+
if (!app) return false;
|
|
34
|
+
const required = Object.entries(app.envSchema).filter(([, f]) => f.required).map(([k]) => k);
|
|
35
|
+
return required.every((k) => (auth.values[k] ?? "").trim().length > 0);
|
|
36
|
+
};
|
|
37
|
+
const encodeRelPath = (relPath) => relPath.split("/").map(encodeURIComponent).join("/");
|
|
38
|
+
export const useAgentRuntime = () => {
|
|
39
|
+
const cfg = useRuntimeConfig().public.agentRuntime;
|
|
40
|
+
const apiPrefix = (cfg?.apiPrefix ?? "/api/agent-runtime").replace(/\/+$/, "");
|
|
41
|
+
const conversationId = ref(null);
|
|
42
|
+
const messages = shallowRef([]);
|
|
43
|
+
const status = ref("idle");
|
|
44
|
+
const error = ref(null);
|
|
45
|
+
const uiActions = ref([]);
|
|
46
|
+
const app = ref(null);
|
|
47
|
+
const auth = ref(emptyAuth());
|
|
48
|
+
const authReady = computed(() => computeAuthReady(auth.value, app.value));
|
|
49
|
+
onMounted(async () => {
|
|
50
|
+
try {
|
|
51
|
+
const info = await $fetch(`${apiPrefix}/app`);
|
|
52
|
+
app.value = info;
|
|
53
|
+
auth.value = loadAuth(info.appId);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.warn("[agent-runtime] failed to load app manifest", err);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
const files = ref([]);
|
|
59
|
+
const fileUrl = (relPath) => {
|
|
60
|
+
const cid = conversationId.value;
|
|
61
|
+
if (!cid) return "";
|
|
62
|
+
return `${apiPrefix}/conversations/${cid}/files/raw/${encodeRelPath(relPath)}`;
|
|
63
|
+
};
|
|
64
|
+
const refreshFiles = async () => {
|
|
65
|
+
const cid = conversationId.value;
|
|
66
|
+
if (!cid) {
|
|
67
|
+
files.value = [];
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const res = await $fetch(`${apiPrefix}/conversations/${cid}/files`);
|
|
72
|
+
files.value = res.items;
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn("[agent-runtime] file list failed", err);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
let abortController = null;
|
|
78
|
+
let currentAssistantId = null;
|
|
79
|
+
const replaceMessages = (next) => {
|
|
80
|
+
messages.value = next;
|
|
81
|
+
};
|
|
82
|
+
const updateAssistant = (mutate) => {
|
|
83
|
+
if (!currentAssistantId) return;
|
|
84
|
+
const id = currentAssistantId;
|
|
85
|
+
replaceMessages(messages.value.map((m) => m.id === id ? mutate(m) : m));
|
|
86
|
+
};
|
|
87
|
+
const ensureAssistant = () => {
|
|
88
|
+
if (currentAssistantId) return;
|
|
89
|
+
currentAssistantId = newId();
|
|
90
|
+
replaceMessages([
|
|
91
|
+
...messages.value,
|
|
92
|
+
{ id: currentAssistantId, role: "assistant", parts: [] }
|
|
93
|
+
]);
|
|
94
|
+
};
|
|
95
|
+
const appendDelta = (kind, delta) => {
|
|
96
|
+
ensureAssistant();
|
|
97
|
+
updateAssistant((msg) => {
|
|
98
|
+
const parts = msg.parts.slice();
|
|
99
|
+
const last = parts[parts.length - 1];
|
|
100
|
+
if (last && last.type === kind) {
|
|
101
|
+
parts[parts.length - 1] = { ...last, text: (last.text ?? "") + delta };
|
|
102
|
+
} else {
|
|
103
|
+
parts.push({ type: kind, text: delta });
|
|
104
|
+
}
|
|
105
|
+
return { ...msg, parts };
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
const upsertToolPart = (toolName, toolCallId, mut) => {
|
|
109
|
+
ensureAssistant();
|
|
110
|
+
updateAssistant((msg) => {
|
|
111
|
+
const parts = msg.parts.slice();
|
|
112
|
+
const idx = parts.findIndex((p) => p.type === `tool-${toolName}` && p.toolCallId === toolCallId);
|
|
113
|
+
const next = mut(idx >= 0 ? parts[idx] : void 0);
|
|
114
|
+
if (idx >= 0) parts[idx] = next;
|
|
115
|
+
else parts.push(next);
|
|
116
|
+
return { ...msg, parts };
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
const onSseEvent = (eventName, data) => {
|
|
120
|
+
switch (eventName) {
|
|
121
|
+
case "agent_start":
|
|
122
|
+
status.value = "streaming";
|
|
123
|
+
break;
|
|
124
|
+
case "delta": {
|
|
125
|
+
const d = data;
|
|
126
|
+
appendDelta(d.kind === "thinking" ? "reasoning" : "text", d.delta ?? "");
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "tool_start": {
|
|
130
|
+
const d = data;
|
|
131
|
+
upsertToolPart(d.toolName, d.toolCallId, () => ({
|
|
132
|
+
type: `tool-${d.toolName}`,
|
|
133
|
+
toolCallId: d.toolCallId,
|
|
134
|
+
state: "input-available",
|
|
135
|
+
input: d.args
|
|
136
|
+
}));
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case "tool_end": {
|
|
140
|
+
const d = data;
|
|
141
|
+
upsertToolPart(d.toolName, d.toolCallId, (prev) => ({
|
|
142
|
+
...prev ?? { type: `tool-${d.toolName}`, toolCallId: d.toolCallId, input: void 0, state: "input-available" },
|
|
143
|
+
type: `tool-${d.toolName}`,
|
|
144
|
+
toolCallId: d.toolCallId,
|
|
145
|
+
state: d.isError ? "output-error" : "output-available",
|
|
146
|
+
output: d.isError ? void 0 : "ok",
|
|
147
|
+
errorText: d.isError ? "Tool returned an error" : void 0
|
|
148
|
+
}));
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "ui_action": {
|
|
152
|
+
const d = data;
|
|
153
|
+
uiActions.value = [{ toolCallId: d.toolCallId, type: d.type, payload: d.payload, at: Date.now() }, ...uiActions.value].slice(0, 50);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "end":
|
|
157
|
+
status.value = "ready";
|
|
158
|
+
currentAssistantId = null;
|
|
159
|
+
void refreshFiles();
|
|
160
|
+
break;
|
|
161
|
+
case "error": {
|
|
162
|
+
const d = data;
|
|
163
|
+
error.value = new Error(d?.message ?? "stream error");
|
|
164
|
+
status.value = "error";
|
|
165
|
+
currentAssistantId = null;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
default:
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const consume = async (signal) => {
|
|
173
|
+
const res = await fetch(`${apiPrefix}/conversations/${conversationId.value}/stream`, { signal });
|
|
174
|
+
if (!res.ok || !res.body) throw new Error(`stream failed: ${res.status}`);
|
|
175
|
+
const reader = res.body.getReader();
|
|
176
|
+
const decoder = new TextDecoder();
|
|
177
|
+
let buffer = "";
|
|
178
|
+
while (true) {
|
|
179
|
+
const { value, done } = await reader.read();
|
|
180
|
+
if (done) break;
|
|
181
|
+
buffer += decoder.decode(value, { stream: true });
|
|
182
|
+
let idx;
|
|
183
|
+
while ((idx = buffer.indexOf("\n\n")) >= 0) {
|
|
184
|
+
const frame = buffer.slice(0, idx);
|
|
185
|
+
buffer = buffer.slice(idx + 2);
|
|
186
|
+
if (!frame.trim()) continue;
|
|
187
|
+
let event = "message";
|
|
188
|
+
const dataLines = [];
|
|
189
|
+
for (const raw of frame.split("\n")) {
|
|
190
|
+
if (raw.startsWith(":")) continue;
|
|
191
|
+
if (raw.startsWith("event:")) event = raw.slice(6).trim();
|
|
192
|
+
else if (raw.startsWith("data:")) dataLines.push(raw.slice(5).trim());
|
|
193
|
+
}
|
|
194
|
+
if (dataLines.length === 0) continue;
|
|
195
|
+
const dataStr = dataLines.join("\n");
|
|
196
|
+
let data = dataStr;
|
|
197
|
+
try {
|
|
198
|
+
data = JSON.parse(dataStr);
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
onSseEvent(event, data);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const start = async (options = {}) => {
|
|
206
|
+
if (!authReady.value) {
|
|
207
|
+
const missing = app.value ? Object.entries(app.value.envSchema).filter(([k, f]) => f.required && !(auth.value.values[k] ?? "").trim()).map(([k]) => k) : [];
|
|
208
|
+
throw new Error(
|
|
209
|
+
missing.length > 0 ? `Fill in required env vars before starting a chat: ${missing.join(", ")}` : "App manifest not loaded yet. Try again in a moment."
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
error.value = null;
|
|
213
|
+
const res = await $fetch(`${apiPrefix}/conversations`, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
body: { env: buildEnvFromAuth(auth.value), language: options.language }
|
|
216
|
+
});
|
|
217
|
+
conversationId.value = res.conversationId;
|
|
218
|
+
abortController?.abort();
|
|
219
|
+
abortController = new AbortController();
|
|
220
|
+
const signal = abortController.signal;
|
|
221
|
+
consume(signal).catch((err) => {
|
|
222
|
+
if (signal.aborted) return;
|
|
223
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
224
|
+
status.value = "error";
|
|
225
|
+
});
|
|
226
|
+
void refreshFiles();
|
|
227
|
+
return res.conversationId;
|
|
228
|
+
};
|
|
229
|
+
const saveAuth = async (next) => {
|
|
230
|
+
auth.value = { values: { ...next.values } };
|
|
231
|
+
if (app.value) persistAuth(app.value.appId, auth.value);
|
|
232
|
+
if (conversationId.value) {
|
|
233
|
+
await $fetch(`${apiPrefix}/conversations/${conversationId.value}/env`, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
body: { env: buildEnvFromAuth(auth.value), merge: true }
|
|
236
|
+
}).catch((err) => {
|
|
237
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const send = async (text, options = {}) => {
|
|
242
|
+
if (!text.trim()) return;
|
|
243
|
+
if (!conversationId.value) {
|
|
244
|
+
try {
|
|
245
|
+
await start({ language: options.language });
|
|
246
|
+
} catch (err) {
|
|
247
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
248
|
+
status.value = "error";
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
error.value = null;
|
|
253
|
+
status.value = "submitted";
|
|
254
|
+
currentAssistantId = null;
|
|
255
|
+
replaceMessages([
|
|
256
|
+
...messages.value,
|
|
257
|
+
{ id: newId(), role: "user", parts: [{ type: "text", text }] }
|
|
258
|
+
]);
|
|
259
|
+
const wireContent = options.rewriteContent ? options.rewriteContent(text) : text;
|
|
260
|
+
await $fetch(`${apiPrefix}/conversations/${conversationId.value}/messages`, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
body: { content: wireContent, context: options.context, language: options.language }
|
|
263
|
+
}).catch((err) => {
|
|
264
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
265
|
+
status.value = "error";
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
const abort = async () => {
|
|
269
|
+
if (!conversationId.value) return;
|
|
270
|
+
await $fetch(`${apiPrefix}/conversations/${conversationId.value}/abort`, { method: "POST" }).catch(() => {
|
|
271
|
+
});
|
|
272
|
+
status.value = "ready";
|
|
273
|
+
};
|
|
274
|
+
const reset = async () => {
|
|
275
|
+
abortController?.abort();
|
|
276
|
+
abortController = null;
|
|
277
|
+
conversationId.value = null;
|
|
278
|
+
currentAssistantId = null;
|
|
279
|
+
replaceMessages([]);
|
|
280
|
+
uiActions.value = [];
|
|
281
|
+
files.value = [];
|
|
282
|
+
status.value = "idle";
|
|
283
|
+
error.value = null;
|
|
284
|
+
};
|
|
285
|
+
return {
|
|
286
|
+
conversationId,
|
|
287
|
+
// Cast at the boundary: AgentRuntimeMessage is a structurally-compatible subset of
|
|
288
|
+
// the AI SDK's UIMessage, but the latter's tagged union is too narrow for
|
|
289
|
+
// TypeScript to accept the cast implicitly.
|
|
290
|
+
messages,
|
|
291
|
+
status,
|
|
292
|
+
error,
|
|
293
|
+
uiActions,
|
|
294
|
+
app,
|
|
295
|
+
auth,
|
|
296
|
+
authReady,
|
|
297
|
+
files,
|
|
298
|
+
fileUrl,
|
|
299
|
+
refreshFiles,
|
|
300
|
+
saveAuth,
|
|
301
|
+
start,
|
|
302
|
+
send,
|
|
303
|
+
abort,
|
|
304
|
+
reset
|
|
305
|
+
};
|
|
306
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import MarkdownIt from 'markdown-it';
|
|
2
|
+
export interface AgentRuntimeMarkdownRenderOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a workspace-relative path (e.g. `outputs/foo.pdf`) to a fetchable
|
|
5
|
+
* URL on the runtime backend. Returns null/empty if the path can't be
|
|
6
|
+
* resolved, in which case the renderer falls back to the original text.
|
|
7
|
+
*/
|
|
8
|
+
resolveWorkspacePath?: (relPath: string) => string | null | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare const createAgentRuntimeMarkdownRenderer: (options?: AgentRuntimeMarkdownRenderOptions) => MarkdownIt;
|
|
11
|
+
export declare const useAgentRuntimeMarkdown: (options?: AgentRuntimeMarkdownRenderOptions) => {
|
|
12
|
+
markdown: MarkdownIt;
|
|
13
|
+
render: (text: string) => string;
|
|
14
|
+
renderInline: (text: string) => string;
|
|
15
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
import {
|
|
3
|
+
isAgentRuntimeImagePath,
|
|
4
|
+
resolveAgentRuntimeWorkspaceUri,
|
|
5
|
+
toAgentRuntimeWorkspaceRelativePath
|
|
6
|
+
} from "../utils/files.js";
|
|
7
|
+
const BARE_WORKSPACE_PATH_RE = /(sandbox:[^\s)\]"'<>]+|\/workspace\/[^\s)\]"'<>]+)/g;
|
|
8
|
+
export const createAgentRuntimeMarkdownRenderer = (options = {}) => {
|
|
9
|
+
const md = new MarkdownIt({
|
|
10
|
+
html: false,
|
|
11
|
+
linkify: true,
|
|
12
|
+
breaks: true,
|
|
13
|
+
typographer: false
|
|
14
|
+
});
|
|
15
|
+
const resolve = (uri) => resolveAgentRuntimeWorkspaceUri(uri, options.resolveWorkspacePath);
|
|
16
|
+
const defaultLinkOpen = md.renderer.rules.link_open ?? ((tokens, idx, opts, _env, self) => self.renderToken(tokens, idx, opts));
|
|
17
|
+
md.renderer.rules.link_open = (tokens, idx, opts, env, self) => {
|
|
18
|
+
const token = tokens[idx];
|
|
19
|
+
if (token) {
|
|
20
|
+
const href = token.attrGet("href") ?? "";
|
|
21
|
+
const rewritten = resolve(href);
|
|
22
|
+
if (rewritten) {
|
|
23
|
+
token.attrSet("href", rewritten);
|
|
24
|
+
token.attrSet("target", "_blank");
|
|
25
|
+
token.attrSet("rel", "noopener noreferrer");
|
|
26
|
+
token.attrSet("class", "agent-runtime-md-file-link");
|
|
27
|
+
} else if (/^https?:\/\//i.test(href)) {
|
|
28
|
+
token.attrSet("target", "_blank");
|
|
29
|
+
token.attrSet("rel", "noopener noreferrer");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return defaultLinkOpen(tokens, idx, opts, env, self);
|
|
33
|
+
};
|
|
34
|
+
md.renderer.rules.image = (tokens, idx) => {
|
|
35
|
+
const token = tokens[idx];
|
|
36
|
+
if (!token) return "";
|
|
37
|
+
const rawSrc = token.attrGet("src") ?? "";
|
|
38
|
+
const rewritten = resolve(rawSrc);
|
|
39
|
+
const src = rewritten ?? rawSrc;
|
|
40
|
+
const alt = token.content || "";
|
|
41
|
+
const title = token.attrGet("title");
|
|
42
|
+
const titleAttr = title ? ` title="${md.utils.escapeHtml(title)}"` : "";
|
|
43
|
+
const safeAlt = md.utils.escapeHtml(alt);
|
|
44
|
+
const safeSrc = md.utils.escapeHtml(src);
|
|
45
|
+
const looksLikeImage = isAgentRuntimeImagePath(rawSrc) || isAgentRuntimeImagePath(src);
|
|
46
|
+
if (!looksLikeImage && rewritten) {
|
|
47
|
+
return `<a href="${safeSrc}" target="_blank" rel="noopener noreferrer" class="agent-runtime-md-file-link">${safeAlt || safeSrc}</a>`;
|
|
48
|
+
}
|
|
49
|
+
return `<a href="${safeSrc}" target="_blank" rel="noopener noreferrer" class="agent-runtime-md-image-link"><img src="${safeSrc}" alt="${safeAlt}" loading="lazy" class="agent-runtime-md-image"${titleAttr} /></a>`;
|
|
50
|
+
};
|
|
51
|
+
md.core.ruler.after("inline", "agent_runtime_workspace_autolink", (state) => {
|
|
52
|
+
for (const blockToken of state.tokens) {
|
|
53
|
+
if (blockToken.type !== "inline" || !blockToken.children) continue;
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const child of blockToken.children) {
|
|
56
|
+
if (child.type !== "text") {
|
|
57
|
+
out.push(child);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const text = child.content;
|
|
61
|
+
BARE_WORKSPACE_PATH_RE.lastIndex = 0;
|
|
62
|
+
if (!BARE_WORKSPACE_PATH_RE.test(text)) {
|
|
63
|
+
out.push(child);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
BARE_WORKSPACE_PATH_RE.lastIndex = 0;
|
|
67
|
+
let lastIndex = 0;
|
|
68
|
+
let match;
|
|
69
|
+
while ((match = BARE_WORKSPACE_PATH_RE.exec(text)) !== null) {
|
|
70
|
+
const uri = match[0];
|
|
71
|
+
const url = resolve(uri);
|
|
72
|
+
if (!url) continue;
|
|
73
|
+
if (match.index > lastIndex) {
|
|
74
|
+
const before = new state.Token("text", "", 0);
|
|
75
|
+
before.content = text.slice(lastIndex, match.index);
|
|
76
|
+
out.push(before);
|
|
77
|
+
}
|
|
78
|
+
const open = new state.Token("link_open", "a", 1);
|
|
79
|
+
open.attrSet("href", url);
|
|
80
|
+
open.attrSet("target", "_blank");
|
|
81
|
+
open.attrSet("rel", "noopener noreferrer");
|
|
82
|
+
open.attrSet("class", "agent-runtime-md-file-link");
|
|
83
|
+
const inner = new state.Token("text", "", 0);
|
|
84
|
+
const relPath = toAgentRuntimeWorkspaceRelativePath(uri) ?? uri;
|
|
85
|
+
inner.content = relPath.split("/").pop() || relPath;
|
|
86
|
+
const close = new state.Token("link_close", "a", -1);
|
|
87
|
+
out.push(open, inner, close);
|
|
88
|
+
lastIndex = match.index + uri.length;
|
|
89
|
+
}
|
|
90
|
+
if (lastIndex < text.length) {
|
|
91
|
+
const tail = new state.Token("text", "", 0);
|
|
92
|
+
tail.content = text.slice(lastIndex);
|
|
93
|
+
out.push(tail);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
blockToken.children = out;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
});
|
|
100
|
+
return md;
|
|
101
|
+
};
|
|
102
|
+
export const useAgentRuntimeMarkdown = (options = {}) => {
|
|
103
|
+
const markdown = createAgentRuntimeMarkdownRenderer(options);
|
|
104
|
+
return {
|
|
105
|
+
markdown,
|
|
106
|
+
render: (text) => text ? markdown.render(text) : "",
|
|
107
|
+
renderInline: (text) => text ? markdown.renderInline(text) : ""
|
|
108
|
+
};
|
|
109
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { agentRuntime, agentRuntimeHeaders } from "../utils/agent-runtime.js";
|
|
2
|
+
export default defineEventHandler(async () => {
|
|
3
|
+
const cfg = agentRuntime();
|
|
4
|
+
const response = await fetch(`${cfg.baseUrl}/v1/apps`, {
|
|
5
|
+
headers: agentRuntimeHeaders(cfg)
|
|
6
|
+
});
|
|
7
|
+
if (!response.ok) {
|
|
8
|
+
throw createError({
|
|
9
|
+
statusCode: response.status,
|
|
10
|
+
statusMessage: await response.text()
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
const res = await response.json();
|
|
14
|
+
const app = res.apps.find((a) => a.appId === cfg.appId);
|
|
15
|
+
if (!app) {
|
|
16
|
+
throw createError({
|
|
17
|
+
statusCode: 502,
|
|
18
|
+
statusMessage: `agent-runtime has no app with id "${cfg.appId}"`
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
appId: app.appId,
|
|
23
|
+
name: app.name,
|
|
24
|
+
envSchema: app.envSchema ?? {}
|
|
25
|
+
};
|
|
26
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { agentRuntime, agentRuntimeHeaders } from "../../../utils/agent-runtime.js";
|
|
2
|
+
export default defineEventHandler(async (event) => {
|
|
3
|
+
const cfg = agentRuntime();
|
|
4
|
+
const id = getRouterParam(event, "id");
|
|
5
|
+
if (!id) throw createError({ statusCode: 400, statusMessage: "missing conversation id" });
|
|
6
|
+
const response = await fetch(`${cfg.baseUrl}/v1/conversations/${id}/abort`, {
|
|
7
|
+
method: "POST",
|
|
8
|
+
headers: agentRuntimeHeaders(cfg)
|
|
9
|
+
});
|
|
10
|
+
if (!response.ok) {
|
|
11
|
+
throw createError({
|
|
12
|
+
statusCode: response.status,
|
|
13
|
+
statusMessage: await response.text()
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
setResponseStatus(event, 204);
|
|
17
|
+
return null;
|
|
18
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST `${apiPrefix}/conversations/:id/env` — patch the workspace env of an
|
|
3
|
+
* existing conversation. `merge: true` (the default) merges with the current
|
|
4
|
+
* env; `merge: false` replaces it entirely.
|
|
5
|
+
*
|
|
6
|
+
* Triggers any `bootstrap[]` step on the harness side whose `rerunOn` keys
|
|
7
|
+
* intersect the changed set, so this is also how you re-import auth.
|
|
8
|
+
*/
|
|
9
|
+
declare const _default: any;
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { agentRuntime, agentRuntimeHeaders } from "../../../utils/agent-runtime.js";
|
|
2
|
+
export default defineEventHandler(async (event) => {
|
|
3
|
+
const cfg = agentRuntime();
|
|
4
|
+
const id = getRouterParam(event, "id");
|
|
5
|
+
if (!id) throw createError({ statusCode: 400, statusMessage: "missing conversation id" });
|
|
6
|
+
const body = await readBody(event).catch(() => ({}));
|
|
7
|
+
if (!body.env || typeof body.env !== "object") {
|
|
8
|
+
throw createError({ statusCode: 400, statusMessage: "missing env" });
|
|
9
|
+
}
|
|
10
|
+
const response = await fetch(`${cfg.baseUrl}/v1/conversations/${id}/env`, {
|
|
11
|
+
method: "POST",
|
|
12
|
+
headers: agentRuntimeHeaders(cfg),
|
|
13
|
+
body: JSON.stringify({ env: body.env, merge: body.merge ?? true })
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
throw createError({
|
|
17
|
+
statusCode: response.status,
|
|
18
|
+
statusMessage: await response.text()
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return await response.json();
|
|
22
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET `${apiPrefix}/conversations/:id/files/raw/<path>` — streams a single
|
|
3
|
+
* workspace file straight back to the browser.
|
|
4
|
+
*
|
|
5
|
+
* We intentionally read the path tail off the *raw* request URL rather than
|
|
6
|
+
* `getRouterParam` so the bytes (already URL-encoded by the browser) are
|
|
7
|
+
* forwarded verbatim — re-encoding after Nitro's decoding step is fragile
|
|
8
|
+
* for filenames containing spaces or unicode.
|
|
9
|
+
*/
|
|
10
|
+
declare const _default: any;
|
|
11
|
+
export default _default;
|