mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-22 08:03:38 +00:00
refactor: simplify global ai draft items
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
d50f8adc44
commit
1ea54ca592
@@ -22,12 +22,12 @@ Global mode should be a new AI chat mode with a small curated tool surface:
|
||||
|
||||
2. `list_workspace_items`
|
||||
- Lists current workspace scripts, flows, and AI drafts.
|
||||
- Returns metadata only: `type`, `path`, `summary`, draft status, and size/version hints.
|
||||
- Returns metadata only: `type`, `path`, `summary`, `language`, and `isDraft`.
|
||||
- Does not include source code or flow JSON.
|
||||
|
||||
3. `read_workspace_item`
|
||||
- Reads one item by `type` and `path`.
|
||||
- If a draft exists, returns the draft and its base metadata.
|
||||
- If a draft exists, returns the draft item.
|
||||
- Otherwise reads from the workspace through generated service clients.
|
||||
- Initial supported types: `script`, `flow`.
|
||||
|
||||
@@ -38,7 +38,7 @@ Global mode should be a new AI chat mode with a small curated tool surface:
|
||||
|
||||
5. `modify_workspace_item`
|
||||
- Creates or updates a draft overlay for an existing item.
|
||||
- Requires the existing item to be read first so the draft can keep base metadata.
|
||||
- Requires the existing item to be read first so the model can produce a complete replacement value.
|
||||
- Does not call backend create/update APIs.
|
||||
|
||||
## Draft Store Shape
|
||||
@@ -47,27 +47,18 @@ Create a frontend store for AI workspace drafts. Suggested file:
|
||||
|
||||
- `frontend/src/lib/components/copilot/chat/global/draftStore.svelte.ts`
|
||||
|
||||
Suggested draft model:
|
||||
Suggested item/draft model:
|
||||
|
||||
```ts
|
||||
type GlobalDraftItemType = 'script' | 'flow'
|
||||
type GlobalWorkspaceItemType = 'script' | 'flow'
|
||||
|
||||
type GlobalDraftStatus = 'new' | 'modified'
|
||||
|
||||
type GlobalDraftBase = {
|
||||
source: 'workspace'
|
||||
version?: string | number
|
||||
editedAt?: string
|
||||
value: unknown
|
||||
}
|
||||
|
||||
type GlobalDraftItem = {
|
||||
type: GlobalDraftItemType
|
||||
type GlobalWorkspaceItem = {
|
||||
type: GlobalWorkspaceItemType
|
||||
path: string
|
||||
status: GlobalDraftStatus
|
||||
base?: GlobalDraftBase
|
||||
draft: unknown
|
||||
updatedAt: string
|
||||
summary?: string
|
||||
value: unknown // script source code or OpenFlow value
|
||||
language?: ScriptLang // required for scripts, omitted for flows
|
||||
isDraft: boolean
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,8 +66,8 @@ Store operations:
|
||||
|
||||
1. `listDrafts()`
|
||||
2. `getDraft(type, path)`
|
||||
3. `setNewDraft(type, path, draft, overwrite?)`
|
||||
4. `setModifiedDraft(type, path, base, draft)`
|
||||
3. `setNewDraft(item, overwrite?)`
|
||||
4. `setModifiedDraft(item)`
|
||||
5. `deleteDraft(type, path)`
|
||||
6. `clearDrafts()`
|
||||
|
||||
@@ -101,17 +92,17 @@ Store operations:
|
||||
|
||||
1. Create `global/draftStore.svelte.ts`.
|
||||
2. Store drafts keyed by `${type}:${path}`.
|
||||
3. Preserve base metadata for modified drafts.
|
||||
3. Store drafts as simple `GlobalWorkspaceItem` values with `isDraft: true`.
|
||||
4. Keep draft operations synchronous and frontend-only.
|
||||
5. Add small utility functions for item keys and normalized timestamps.
|
||||
5. Add small utility functions for item keys.
|
||||
|
||||
### Step 3: Implement Instruction Tool
|
||||
|
||||
1. Add `get_instructions` with a Zod schema:
|
||||
- `subject: 'script' | 'flow'`
|
||||
2. Return concise, practical instructions:
|
||||
- script: language, schema, main function expectations, path conventions;
|
||||
- flow: `OpenFlow` shape, modules, IDs, summaries, validation expectations.
|
||||
- script: language, source-code value, main function expectations, path conventions;
|
||||
- flow: `OpenFlow` value shape, modules, IDs, summaries, validation expectations.
|
||||
3. Keep detailed examples short. This tool can grow later into a workspace skill system.
|
||||
|
||||
### Step 4: Implement Listing Tool
|
||||
@@ -121,10 +112,8 @@ Store operations:
|
||||
- `ScriptService.listScripts`
|
||||
- `FlowService.listFlows`
|
||||
3. Merge drafts from the draft store into the result.
|
||||
4. Mark each result:
|
||||
- `source: 'workspace' | 'draft' | 'workspace+draft'`
|
||||
- `draftStatus?: 'new' | 'modified'`
|
||||
5. Return compact metadata only.
|
||||
4. Mark drafts with `isDraft: true`.
|
||||
5. Return compact metadata only without `value`.
|
||||
|
||||
### Step 5: Implement Read Tool
|
||||
|
||||
@@ -133,12 +122,7 @@ Store operations:
|
||||
3. If no draft exists:
|
||||
- scripts: `ScriptService.getScriptByPath`
|
||||
- flows: `FlowService.getFlowByPath`
|
||||
4. Normalize output with:
|
||||
- `type`
|
||||
- `path`
|
||||
- `source`
|
||||
- `baseVersion`
|
||||
- `value`
|
||||
4. Normalize output as `{ type, path, summary, value, language, isDraft }` where `value` is script source code or the OpenFlow value.
|
||||
|
||||
### Step 6: Implement Write Draft Tool
|
||||
|
||||
@@ -148,17 +132,16 @@ Store operations:
|
||||
- existing draft with same key;
|
||||
- existing workspace item with same type/path.
|
||||
4. If conflict exists and `overwrite` is not true, return a clear error.
|
||||
5. Store a `status: 'new'` draft.
|
||||
5. Store the draft as a `GlobalWorkspaceItem` with `isDraft: true`.
|
||||
6. Return a short summary that explicitly says the workspace was not saved.
|
||||
|
||||
### Step 7: Implement Modify Draft Tool
|
||||
|
||||
1. Add `modify_workspace_item`.
|
||||
2. Require existing draft or existing workspace item.
|
||||
3. If no draft exists, fetch workspace item and store it as `base`.
|
||||
4. Store a `status: 'modified'` draft with the new draft value.
|
||||
5. Return base version/hash metadata and a clear "draft only" message.
|
||||
6. Consider requiring `baseVersion` in a follow-up after the POC to reduce stale edits.
|
||||
3. Store the modified item as a draft with `isDraft: true`.
|
||||
4. Return a clear "draft only" message.
|
||||
5. Consider adding base version/hash checks in a follow-up after the POC to reduce stale edits.
|
||||
|
||||
### Step 8: Wire UI Entry Point
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
case AIMode.API:
|
||||
return 'Make API calls...'
|
||||
case AIMode.GLOBAL:
|
||||
return 'Work across workspace items...'
|
||||
return 'Work across scripts and flows...'
|
||||
case AIMode.ASK:
|
||||
return 'Ask questions about Windmill...'
|
||||
default:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import { $ScriptLang, FlowService, ScriptService } from '$lib/gen'
|
||||
import { getFlowPrompt, getScriptPrompt } from '$system_prompts'
|
||||
import type { Flow, Script } from '$lib/gen/types.gen'
|
||||
import type { Flow, Script, ScriptLang } from '$lib/gen/types.gen'
|
||||
import type {
|
||||
ChatCompletionSystemMessageParam,
|
||||
ChatCompletionUserMessageParam
|
||||
@@ -10,72 +10,20 @@ import { createToolDef, type Tool } from '../shared'
|
||||
import {
|
||||
globalDraftStore,
|
||||
getGlobalDraftKey,
|
||||
type GlobalDraftBase,
|
||||
type GlobalDraftItem,
|
||||
type GlobalWorkspaceItem,
|
||||
type GlobalWorkspaceItemType
|
||||
} from './draftStore.svelte'
|
||||
|
||||
const ITEM_TYPES = ['script', 'flow'] as const satisfies readonly GlobalWorkspaceItemType[]
|
||||
const MAX_LIST_LIMIT = 100
|
||||
|
||||
const SCRIPT_INSTRUCTION_LANGUAGES = [
|
||||
'bunnative',
|
||||
'nativets',
|
||||
'bun',
|
||||
'deno',
|
||||
'python3',
|
||||
'php',
|
||||
'rust',
|
||||
'go',
|
||||
'bash',
|
||||
'postgresql',
|
||||
'mysql',
|
||||
'bigquery',
|
||||
'snowflake',
|
||||
'mssql',
|
||||
'graphql',
|
||||
'powershell',
|
||||
'csharp',
|
||||
'java',
|
||||
'duckdb',
|
||||
'rlang',
|
||||
'oracledb',
|
||||
'ansible',
|
||||
'nu',
|
||||
'ruby'
|
||||
] as const
|
||||
|
||||
const SYSTEM_PROMPT_SCRIPT_LANGUAGES = new Set<string>([
|
||||
'bunnative',
|
||||
'nativets',
|
||||
'bun',
|
||||
'deno',
|
||||
'python3',
|
||||
'php',
|
||||
'rust',
|
||||
'go',
|
||||
'bash',
|
||||
'postgresql',
|
||||
'mysql',
|
||||
'bigquery',
|
||||
'snowflake',
|
||||
'mssql',
|
||||
'graphql',
|
||||
'powershell',
|
||||
'csharp',
|
||||
'java',
|
||||
'duckdb',
|
||||
'rlang'
|
||||
])
|
||||
const DEFAULT_SCRIPT_LANGUAGE: ScriptLang = 'bun'
|
||||
|
||||
const itemTypeSchema = z.enum(ITEM_TYPES)
|
||||
const scriptInstructionLanguageSchema = z.enum(SCRIPT_INSTRUCTION_LANGUAGES)
|
||||
type ScriptInstructionLanguage = z.infer<typeof scriptInstructionLanguageSchema>
|
||||
const DEFAULT_SCRIPT_LANGUAGE: ScriptInstructionLanguage = 'bun'
|
||||
const scriptLanguageSchema = z.enum($ScriptLang.enum)
|
||||
|
||||
const getInstructionsSchema = z.object({
|
||||
subject: itemTypeSchema.describe('The workspace item type to get authoring instructions for.'),
|
||||
language: scriptInstructionLanguageSchema
|
||||
language: scriptLanguageSchema
|
||||
.optional()
|
||||
.describe(
|
||||
'Required when subject is script. Use the existing script language when modifying, or the requested target language when creating.'
|
||||
@@ -106,44 +54,45 @@ const readWorkspaceItemSchema = z.object({
|
||||
path: z.string().describe('Workspace path of the item to read.')
|
||||
})
|
||||
|
||||
const writeWorkspaceItemSchema = z.object({
|
||||
const workspaceItemDraftBaseSchema = z.object({
|
||||
type: itemTypeSchema,
|
||||
path: z.string().describe('Workspace path for the new draft item.'),
|
||||
draft: z.any().describe('The complete draft item payload for the item type.'),
|
||||
overwrite: z
|
||||
.boolean()
|
||||
path: z.string().describe('Workspace path of the item.'),
|
||||
summary: z.string().optional().describe('Short summary of the item.'),
|
||||
value: z
|
||||
.any()
|
||||
.describe('For scripts, the complete script source code. For flows, the complete OpenFlow value.'),
|
||||
language: scriptLanguageSchema
|
||||
.optional()
|
||||
.describe('If true, replace an existing AI draft with the same type and path.')
|
||||
.describe('Required for script items. Omit for flow items.')
|
||||
})
|
||||
|
||||
const modifyWorkspaceItemSchema = z.object({
|
||||
type: itemTypeSchema,
|
||||
path: z.string().describe('Workspace path of the existing item or AI draft to modify.'),
|
||||
draft: z.any().describe('The complete modified draft item payload for the item type.')
|
||||
})
|
||||
type WorkspaceItemDraftInput = z.infer<typeof workspaceItemDraftBaseSchema>
|
||||
|
||||
type WorkspaceItemRead = {
|
||||
type: GlobalWorkspaceItemType
|
||||
path: string
|
||||
summary?: string
|
||||
version?: string | number
|
||||
editedAt?: string
|
||||
value: unknown
|
||||
function validateWorkspaceItemDraft(
|
||||
item: WorkspaceItemDraftInput,
|
||||
ctx: z.RefinementCtx
|
||||
): void {
|
||||
if (item.type === 'script' && !item.language) {
|
||||
ctx.addIssue({
|
||||
code: 'custom',
|
||||
path: ['language'],
|
||||
message: 'language is required for script items'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type WorkspaceItemMetadata = {
|
||||
type: GlobalWorkspaceItemType
|
||||
path: string
|
||||
source: 'workspace' | 'draft' | 'workspace+draft'
|
||||
summary?: string
|
||||
version?: string | number
|
||||
editedAt?: string
|
||||
draftStatus?: GlobalDraftItem['status']
|
||||
size?: number
|
||||
language?: string
|
||||
kind?: string
|
||||
moduleCount?: number
|
||||
}
|
||||
const writeWorkspaceItemSchema = workspaceItemDraftBaseSchema
|
||||
.extend({
|
||||
overwrite: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('If true, replace an existing AI draft with the same type and path.')
|
||||
})
|
||||
.superRefine(validateWorkspaceItemDraft)
|
||||
|
||||
const modifyWorkspaceItemSchema = workspaceItemDraftBaseSchema.superRefine(validateWorkspaceItemDraft)
|
||||
|
||||
type WorkspaceItemMetadata = Omit<GlobalWorkspaceItem, 'value'>
|
||||
|
||||
const GLOBAL_SYSTEM_PROMPT = `You are Windmill's global workspace assistant.
|
||||
|
||||
@@ -151,6 +100,10 @@ You can inspect workspace scripts and flows, then create draft changes in the fr
|
||||
|
||||
Important rules:
|
||||
- Writes and modifications are drafts only. They do not save, deploy, or mutate backend workspace items.
|
||||
- A workspace item is { type, path, summary, value, language, isDraft }.
|
||||
- For scripts, value is the complete script source code and language is required.
|
||||
- For flows, value is the complete OpenFlow value and language is omitted.
|
||||
- list_workspace_items returns metadata only and omits value. Use read_workspace_item for the current value.
|
||||
- Use list_workspace_items before broad reads.
|
||||
- Use read_workspace_item before modifying an existing item unless the user already provided the complete current item.
|
||||
- Use get_instructions before writing or modifying a script or flow. For scripts, pass the target language; when modifying, use the language from the item you read.
|
||||
@@ -163,24 +116,6 @@ function getRequestedTypes(
|
||||
return types && types.length > 0 ? types : [...ITEM_TYPES]
|
||||
}
|
||||
|
||||
function getJsonSize(value: unknown): number | undefined {
|
||||
try {
|
||||
return JSON.stringify(value)?.length
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
function getObjectString(value: unknown, key: string): string | undefined {
|
||||
if (!value || typeof value !== 'object') return undefined
|
||||
const candidate = (value as Record<string, unknown>)[key]
|
||||
return typeof candidate === 'string' ? candidate : undefined
|
||||
}
|
||||
|
||||
function getDraftSummary(draft: GlobalDraftItem): string | undefined {
|
||||
return getObjectString(draft.draft, 'summary') ?? getObjectString(draft.base?.value, 'summary')
|
||||
}
|
||||
|
||||
function itemMatches(
|
||||
item: Pick<WorkspaceItemMetadata, 'path' | 'summary'>,
|
||||
query: string | undefined,
|
||||
@@ -201,83 +136,44 @@ function itemMatches(
|
||||
)
|
||||
}
|
||||
|
||||
function scriptToMetadata(script: Script): WorkspaceItemMetadata {
|
||||
function scriptToWorkspaceItem(script: Script): GlobalWorkspaceItem {
|
||||
return {
|
||||
type: 'script',
|
||||
path: script.path,
|
||||
source: 'workspace',
|
||||
summary: script.summary,
|
||||
version: script.hash,
|
||||
size: script.content?.length,
|
||||
value: script.content,
|
||||
language: script.language,
|
||||
kind: script.kind
|
||||
isDraft: false
|
||||
}
|
||||
}
|
||||
|
||||
function flowToMetadata(flow: Flow): WorkspaceItemMetadata {
|
||||
return {
|
||||
type: 'flow',
|
||||
path: flow.path,
|
||||
source: 'workspace',
|
||||
summary: flow.summary,
|
||||
version: flow.version_id,
|
||||
editedAt: flow.edited_at,
|
||||
moduleCount: flow.value?.modules?.length ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
function draftToMetadata(draft: GlobalDraftItem, hasWorkspaceItem: boolean): WorkspaceItemMetadata {
|
||||
return {
|
||||
type: draft.type,
|
||||
path: draft.path,
|
||||
source: hasWorkspaceItem || draft.status === 'modified' ? 'workspace+draft' : 'draft',
|
||||
summary: getDraftSummary(draft),
|
||||
draftStatus: draft.status,
|
||||
editedAt: draft.updatedAt,
|
||||
size: getJsonSize(draft.draft)
|
||||
}
|
||||
}
|
||||
|
||||
function scriptToRead(script: Script): WorkspaceItemRead {
|
||||
return {
|
||||
type: 'script',
|
||||
path: script.path,
|
||||
summary: script.summary,
|
||||
version: script.hash,
|
||||
value: {
|
||||
path: script.path,
|
||||
summary: script.summary,
|
||||
description: script.description,
|
||||
language: script.language,
|
||||
content: script.content,
|
||||
schema: script.schema,
|
||||
kind: script.kind,
|
||||
tag: script.tag,
|
||||
lock: script.lock,
|
||||
modules: script.modules,
|
||||
envs: script.envs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function flowToRead(flow: Flow): WorkspaceItemRead {
|
||||
function flowToWorkspaceItem(flow: Flow): GlobalWorkspaceItem {
|
||||
return {
|
||||
type: 'flow',
|
||||
path: flow.path,
|
||||
summary: flow.summary,
|
||||
version: flow.version_id,
|
||||
editedAt: flow.edited_at,
|
||||
value: {
|
||||
path: flow.path,
|
||||
summary: flow.summary,
|
||||
description: flow.description,
|
||||
schema: flow.schema,
|
||||
value: flow.value,
|
||||
tag: flow.tag,
|
||||
priority: flow.priority,
|
||||
dedicated_worker: flow.dedicated_worker,
|
||||
timeout: flow.timeout
|
||||
}
|
||||
value: flow.value,
|
||||
isDraft: false
|
||||
}
|
||||
}
|
||||
|
||||
function toMetadata(item: GlobalWorkspaceItem): WorkspaceItemMetadata {
|
||||
return {
|
||||
type: item.type,
|
||||
path: item.path,
|
||||
summary: item.summary,
|
||||
language: item.language,
|
||||
isDraft: item.isDraft
|
||||
}
|
||||
}
|
||||
|
||||
function toDraftInput(item: WorkspaceItemDraftInput): Omit<GlobalWorkspaceItem, 'isDraft'> {
|
||||
return {
|
||||
type: item.type,
|
||||
path: item.path,
|
||||
summary: item.summary,
|
||||
value: item.value,
|
||||
language: item.type === 'script' ? item.language : undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,12 +194,12 @@ async function readWorkspaceItem(
|
||||
type: GlobalWorkspaceItemType,
|
||||
path: string,
|
||||
workspace: string
|
||||
): Promise<WorkspaceItemRead> {
|
||||
): Promise<GlobalWorkspaceItem> {
|
||||
switch (type) {
|
||||
case 'script':
|
||||
return scriptToRead(await ScriptService.getScriptByPath({ workspace, path }))
|
||||
return scriptToWorkspaceItem(await ScriptService.getScriptByPath({ workspace, path }))
|
||||
case 'flow':
|
||||
return flowToRead(await FlowService.getFlowByPath({ workspace, path }))
|
||||
return flowToWorkspaceItem(await FlowService.getFlowByPath({ workspace, path }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +220,7 @@ async function listWorkspaceMetadata(
|
||||
withoutDescription: true
|
||||
})
|
||||
for (const script of scripts) {
|
||||
items.set(getGlobalDraftKey('script', script.path), scriptToMetadata(script))
|
||||
items.set(getGlobalDraftKey('script', script.path), toMetadata(scriptToWorkspaceItem(script)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,30 +233,25 @@ async function listWorkspaceMetadata(
|
||||
withoutDescription: true
|
||||
})
|
||||
for (const flow of flows) {
|
||||
items.set(getGlobalDraftKey('flow', flow.path), flowToMetadata(flow))
|
||||
items.set(getGlobalDraftKey('flow', flow.path), toMetadata(flowToWorkspaceItem(flow)))
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
function getScriptInstructions(language: ScriptInstructionLanguage | undefined): string {
|
||||
function getScriptInstructions(language: ScriptLang | undefined): string {
|
||||
const selectedLanguage = language ?? DEFAULT_SCRIPT_LANGUAGE
|
||||
const languageAvailabilityNote = SYSTEM_PROMPT_SCRIPT_LANGUAGES.has(selectedLanguage)
|
||||
? `Using the shared system_prompts script guidance for \`${selectedLanguage}\`.`
|
||||
: `No dedicated system_prompts language skill exists yet for \`${selectedLanguage}\`; preserve the existing runtime conventions and use the generic Windmill script guidance below.`
|
||||
const defaultLanguageNote = language
|
||||
? ''
|
||||
: `\n- No script language was provided. Default to \`${DEFAULT_SCRIPT_LANGUAGE}\` only for new TypeScript scripts; if the user requested another language or you read an existing script, call get_instructions again with that language.`
|
||||
|
||||
return `# Global draft script instructions
|
||||
|
||||
${languageAvailabilityNote}
|
||||
|
||||
- Global mode writes complete draft payloads only; it does not save, deploy, run, or generate metadata.
|
||||
- Draft payloads for scripts should be NewScript-like objects with: \`path\`, \`summary\`, optional \`description\`, \`language\`, \`content\`, optional JSON Schema \`schema\`, optional \`kind\`, \`tag\`, \`envs\`, \`lock\`, and \`modules\` when needed.
|
||||
- Use workspace paths such as \`f/folder/name\` or \`u/username/name\`. Preserve the current path/language/kind when modifying unless the user asked to change them.
|
||||
- For normal scripts, \`kind\` should be \`script\`; use \`preprocessor\`, \`failure\`, etc. only when that is the actual script role.
|
||||
- Draft payloads for scripts are workspace items: \`{ type: "script", path, summary, value, language }\`.
|
||||
- For scripts, \`value\` is the complete script source code and \`language\` is required.
|
||||
- Use workspace paths such as \`f/folder/name\` or \`u/username/name\`. Preserve the current path/language when modifying unless the user asked to change them.
|
||||
- Return the full desired script draft to \`write_workspace_item\` or \`modify_workspace_item\`, not a patch or partial object.${defaultLanguageNote}
|
||||
|
||||
# Windmill script authoring reference (${selectedLanguage})
|
||||
@@ -372,7 +263,8 @@ function getFlowInstructions(): string {
|
||||
return `# Global draft flow instructions
|
||||
|
||||
- Global mode writes complete draft payloads only; it does not save, deploy, run, scaffold local files, or generate metadata.
|
||||
- Draft payloads for flows should be Flow-like objects with: \`path\`, \`summary\`, optional \`description\`, optional input \`schema\`, and \`value\` containing the OpenFlow value.
|
||||
- Draft payloads for flows are workspace items: \`{ type: "flow", path, summary, value }\`.
|
||||
- For flows, \`value\` is the complete OpenFlow value. Omit \`language\`.
|
||||
- \`value.modules\` contains normal sequential modules. Use top-level \`value.preprocessor_module\` and \`value.failure_module\` for special modules; do not put \`preprocessor\` or \`failure\` in \`value.modules\`.
|
||||
- Every module needs a stable unique \`id\` and a useful \`summary\` when the schema supports it.
|
||||
- Prefer path/script/flow modules when composing existing workspace logic. Use rawscript modules only when new inline code is needed.
|
||||
@@ -386,7 +278,7 @@ ${getFlowPrompt()}`
|
||||
|
||||
function getInstructions(
|
||||
subject: GlobalWorkspaceItemType,
|
||||
language?: ScriptInstructionLanguage
|
||||
language?: ScriptLang
|
||||
): string {
|
||||
switch (subject) {
|
||||
case 'script':
|
||||
@@ -438,12 +330,10 @@ export const globalTools: Tool<{}>[] = [
|
||||
if (!types.includes(draft.type)) continue
|
||||
const key = getGlobalDraftKey(draft.type, draft.path)
|
||||
const existing = workspaceItems.get(key)
|
||||
const draftMetadata = draftToMetadata(draft, existing !== undefined)
|
||||
const draftMetadata = toMetadata(draft)
|
||||
workspaceItems.set(key, {
|
||||
...existing,
|
||||
...draftMetadata,
|
||||
summary: draftMetadata.summary ?? existing?.summary,
|
||||
version: existing?.version ?? draft.base?.version
|
||||
summary: draftMetadata.summary ?? existing?.summary
|
||||
})
|
||||
}
|
||||
|
||||
@@ -470,19 +360,7 @@ export const globalTools: Tool<{}>[] = [
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Read AI draft ${parsedArgs.type} "${parsedArgs.path}"`
|
||||
})
|
||||
return JSON.stringify(
|
||||
{
|
||||
type: draft.type,
|
||||
path: draft.path,
|
||||
source: draft.status === 'new' ? 'draft' : 'workspace+draft',
|
||||
draftStatus: draft.status,
|
||||
baseVersion: draft.base?.version,
|
||||
baseEditedAt: draft.base?.editedAt,
|
||||
draft: draft.draft
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
return JSON.stringify(draft, null, 2)
|
||||
}
|
||||
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
@@ -492,45 +370,41 @@ export const globalTools: Tool<{}>[] = [
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Read ${parsedArgs.type} "${parsedArgs.path}"`
|
||||
})
|
||||
return JSON.stringify({ ...item, source: 'workspace' }, null, 2)
|
||||
return JSON.stringify(item, null, 2)
|
||||
}
|
||||
},
|
||||
{
|
||||
def: createToolDef(
|
||||
writeWorkspaceItemSchema,
|
||||
'write_workspace_item',
|
||||
'Create a new AI draft item. This does not save or deploy anything to the workspace.'
|
||||
'Create a new AI draft workspace item. This does not save or deploy anything to the workspace.'
|
||||
),
|
||||
showDetails: true,
|
||||
streamArguments: true,
|
||||
showFade: true,
|
||||
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
|
||||
const parsedArgs = writeWorkspaceItemSchema.parse(args)
|
||||
const draftInput = toDraftInput(parsedArgs)
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Creating draft ${parsedArgs.type} "${parsedArgs.path}"...`
|
||||
content: `Creating draft ${draftInput.type} "${draftInput.path}"...`
|
||||
})
|
||||
|
||||
if (await workspaceItemExists(parsedArgs.type, parsedArgs.path, workspace)) {
|
||||
const message = `${parsedArgs.type} "${parsedArgs.path}" already exists in the workspace. Use modify_workspace_item to create a draft overlay.`
|
||||
if (await workspaceItemExists(draftInput.type, draftInput.path, workspace)) {
|
||||
const message = `${draftInput.type} "${draftInput.path}" already exists in the workspace. Use modify_workspace_item to create a draft overlay.`
|
||||
toolCallbacks.setToolStatus(toolId, { content: message, error: message })
|
||||
return JSON.stringify({ success: false, error: message })
|
||||
}
|
||||
|
||||
try {
|
||||
const draft = globalDraftStore.setNewDraft(
|
||||
parsedArgs.type,
|
||||
parsedArgs.path,
|
||||
parsedArgs.draft,
|
||||
parsedArgs.overwrite ?? false
|
||||
)
|
||||
const draft = globalDraftStore.setNewDraft(draftInput, parsedArgs.overwrite ?? false)
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Created AI draft ${parsedArgs.type} "${parsedArgs.path}"`,
|
||||
content: `Created AI draft ${draft.type} "${draft.path}"`,
|
||||
result: 'Draft created'
|
||||
})
|
||||
return JSON.stringify(
|
||||
{
|
||||
success: true,
|
||||
message: `Created AI draft ${parsedArgs.type} "${parsedArgs.path}". The workspace was not saved or deployed.`,
|
||||
message: `Created AI draft ${draft.type} "${draft.path}". The workspace was not saved or deployed.`,
|
||||
draft
|
||||
},
|
||||
null,
|
||||
@@ -554,65 +428,27 @@ export const globalTools: Tool<{}>[] = [
|
||||
showFade: true,
|
||||
fn: async ({ args, workspace, toolId, toolCallbacks }) => {
|
||||
const parsedArgs = modifyWorkspaceItemSchema.parse(args)
|
||||
const draftInput = toDraftInput(parsedArgs)
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Updating draft ${parsedArgs.type} "${parsedArgs.path}"...`
|
||||
content: `Updating draft ${draftInput.type} "${draftInput.path}"...`
|
||||
})
|
||||
|
||||
const existingDraft = globalDraftStore.getDraft(parsedArgs.type, parsedArgs.path)
|
||||
if (existingDraft?.status === 'new') {
|
||||
const draft = globalDraftStore.setNewDraft(
|
||||
parsedArgs.type,
|
||||
parsedArgs.path,
|
||||
parsedArgs.draft,
|
||||
true
|
||||
)
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Updated AI draft ${parsedArgs.type} "${parsedArgs.path}"`,
|
||||
result: 'Draft updated'
|
||||
})
|
||||
return JSON.stringify(
|
||||
{
|
||||
success: true,
|
||||
message: `Updated new AI draft ${parsedArgs.type} "${parsedArgs.path}". The workspace was not saved or deployed.`,
|
||||
draft
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
const existingDraft = globalDraftStore.getDraft(draftInput.type, draftInput.path)
|
||||
if (!existingDraft && !(await workspaceItemExists(draftInput.type, draftInput.path, workspace))) {
|
||||
const message = `${draftInput.type} "${draftInput.path}" does not exist in the workspace or AI draft store. Use write_workspace_item for new items.`
|
||||
toolCallbacks.setToolStatus(toolId, { content: message, error: message })
|
||||
return JSON.stringify({ success: false, error: message })
|
||||
}
|
||||
|
||||
let base: GlobalDraftBase | undefined = existingDraft?.base
|
||||
if (!base) {
|
||||
if (!(await workspaceItemExists(parsedArgs.type, parsedArgs.path, workspace))) {
|
||||
const message = `${parsedArgs.type} "${parsedArgs.path}" does not exist in the workspace or AI draft store. Use write_workspace_item for new items.`
|
||||
toolCallbacks.setToolStatus(toolId, { content: message, error: message })
|
||||
return JSON.stringify({ success: false, error: message })
|
||||
}
|
||||
const currentItem = await readWorkspaceItem(parsedArgs.type, parsedArgs.path, workspace)
|
||||
base = {
|
||||
source: 'workspace',
|
||||
version: currentItem.version,
|
||||
editedAt: currentItem.editedAt,
|
||||
value: currentItem.value
|
||||
}
|
||||
}
|
||||
|
||||
const draft = globalDraftStore.setModifiedDraft(
|
||||
parsedArgs.type,
|
||||
parsedArgs.path,
|
||||
base,
|
||||
parsedArgs.draft
|
||||
)
|
||||
const draft = globalDraftStore.setModifiedDraft(draftInput)
|
||||
toolCallbacks.setToolStatus(toolId, {
|
||||
content: `Updated AI draft ${parsedArgs.type} "${parsedArgs.path}"`,
|
||||
content: `Updated AI draft ${draft.type} "${draft.path}"`,
|
||||
result: 'Draft updated'
|
||||
})
|
||||
return JSON.stringify(
|
||||
{
|
||||
success: true,
|
||||
message: `Updated AI draft ${parsedArgs.type} "${parsedArgs.path}". The workspace was not saved or deployed.`,
|
||||
baseVersion: base.version,
|
||||
baseEditedAt: base.editedAt,
|
||||
message: `Updated AI draft ${draft.type} "${draft.path}". The workspace was not saved or deployed.`,
|
||||
draft
|
||||
},
|
||||
null,
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
import type { ScriptLang } from '$lib/gen/types.gen'
|
||||
|
||||
export type GlobalWorkspaceItemType = 'script' | 'flow'
|
||||
|
||||
export type GlobalDraftStatus = 'new' | 'modified'
|
||||
|
||||
export type GlobalDraftBase = {
|
||||
source: 'workspace'
|
||||
version?: string | number
|
||||
editedAt?: string
|
||||
value: unknown
|
||||
}
|
||||
|
||||
export type GlobalDraftItem = {
|
||||
export type GlobalWorkspaceItem = {
|
||||
type: GlobalWorkspaceItemType
|
||||
path: string
|
||||
status: GlobalDraftStatus
|
||||
base?: GlobalDraftBase
|
||||
draft: unknown
|
||||
updatedAt: string
|
||||
summary?: string
|
||||
value: unknown
|
||||
language?: ScriptLang
|
||||
isDraft: boolean
|
||||
}
|
||||
|
||||
export function getGlobalDraftKey(type: GlobalWorkspaceItemType, path: string): string {
|
||||
@@ -26,56 +19,41 @@ function cloneValue<T>(value: T): T {
|
||||
return structuredClone($state.snapshot(value)) as T
|
||||
}
|
||||
|
||||
class GlobalDraftStore {
|
||||
private drafts = $state<Record<string, GlobalDraftItem>>({})
|
||||
type DraftInput = Omit<GlobalWorkspaceItem, 'isDraft'>
|
||||
|
||||
listDrafts(): GlobalDraftItem[] {
|
||||
function toDraftItem(input: DraftInput): GlobalWorkspaceItem {
|
||||
return {
|
||||
...cloneValue(input),
|
||||
isDraft: true
|
||||
}
|
||||
}
|
||||
|
||||
class GlobalDraftStore {
|
||||
private drafts = $state<Record<string, GlobalWorkspaceItem>>({})
|
||||
|
||||
listDrafts(): GlobalWorkspaceItem[] {
|
||||
return Object.values(this.drafts).map((draft) => cloneValue(draft))
|
||||
}
|
||||
|
||||
getDraft(type: GlobalWorkspaceItemType, path: string): GlobalDraftItem | undefined {
|
||||
getDraft(type: GlobalWorkspaceItemType, path: string): GlobalWorkspaceItem | undefined {
|
||||
const draft = this.drafts[getGlobalDraftKey(type, path)]
|
||||
return draft ? cloneValue(draft) : undefined
|
||||
}
|
||||
|
||||
setNewDraft(
|
||||
type: GlobalWorkspaceItemType,
|
||||
path: string,
|
||||
draft: unknown,
|
||||
overwrite = false
|
||||
): GlobalDraftItem {
|
||||
const key = getGlobalDraftKey(type, path)
|
||||
setNewDraft(input: DraftInput, overwrite = false): GlobalWorkspaceItem {
|
||||
const key = getGlobalDraftKey(input.type, input.path)
|
||||
if (this.drafts[key] && !overwrite) {
|
||||
throw new Error(`A draft already exists for ${type} "${path}".`)
|
||||
throw new Error(`A draft already exists for ${input.type} "${input.path}".`)
|
||||
}
|
||||
|
||||
const item: GlobalDraftItem = {
|
||||
type,
|
||||
path,
|
||||
status: 'new',
|
||||
draft: cloneValue(draft),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
const item = toDraftItem(input)
|
||||
this.drafts[key] = item
|
||||
return cloneValue(item)
|
||||
}
|
||||
|
||||
setModifiedDraft(
|
||||
type: GlobalWorkspaceItemType,
|
||||
path: string,
|
||||
base: GlobalDraftBase,
|
||||
draft: unknown
|
||||
): GlobalDraftItem {
|
||||
const key = getGlobalDraftKey(type, path)
|
||||
const item: GlobalDraftItem = {
|
||||
type,
|
||||
path,
|
||||
status: 'modified',
|
||||
base: cloneValue(base),
|
||||
draft: cloneValue(draft),
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
this.drafts[key] = item
|
||||
setModifiedDraft(input: DraftInput): GlobalWorkspaceItem {
|
||||
const item = toDraftItem(input)
|
||||
this.drafts[getGlobalDraftKey(input.type, input.path)] = item
|
||||
return cloneValue(item)
|
||||
}
|
||||
|
||||
@@ -87,11 +65,11 @@ class GlobalDraftStore {
|
||||
this.drafts = {}
|
||||
}
|
||||
|
||||
getScriptDraft(path: string): GlobalDraftItem | undefined {
|
||||
getScriptDraft(path: string): GlobalWorkspaceItem | undefined {
|
||||
return this.getDraft('script', path)
|
||||
}
|
||||
|
||||
getFlowDraft(path: string): GlobalDraftItem | undefined {
|
||||
getFlowDraft(path: string): GlobalWorkspaceItem | undefined {
|
||||
return this.getDraft('flow', path)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user