|
27 | 27 | * T:LauncherIo type {stdin,stdout,stderr} override for launcher subprocess |
28 | 28 | * T:JsonSchemaObject type {[key:string]:unknown} plain JSON Schema object |
29 | 29 | * s.string/number/integer/boolean/null SchemaHelperFactory primitives; call as value or fn(desc) |
| 30 | + * s.int alias for s.integer; s.nonEmptyString string with minLength:1; s.url string with format:"uri" |
30 | 31 | * s.array(items,desc?) ArraySchema; use for homogeneous lists, e.g. s.array(s.string) |
31 | 32 | * s.object(props,desc?) ObjectSchema; s.optional(inner) marks field optional; s.nullable(inner) accepts inner|null; use for fixed-key shapes |
32 | 33 | * s.record(valSchema,desc?) RecordSchema keyed by string; use for open-ended key→value maps |
|
36 | 37 | * p`...` PromptBuilder template tag; interpolates PromptIntent|string|PromptBuilder |
37 | 38 | * p.bash(cmd,opts?) PromptIntent bash execution declaration (not run in-process) |
38 | 39 | * p.read(path,opts?) PromptIntent file read declaration |
| 40 | + * p.readOptional(path,fallback?,opts?) PromptIntent file read declaration; returns fallback (default "") if file absent |
39 | 41 | * p.write(path,content,opts?) PromptIntent file write declaration |
40 | 42 | * p.glob(pattern,opts?) PromptIntent glob file-list declaration (not run in-process) |
| 43 | + * p.env(name,fallback?,opts?) PromptIntent env var read declaration; returns fallback (default "") if not set |
41 | 44 | * p.json(value) string JSON.stringify helper for inlining structured values in prompt templates |
42 | 45 | * F:agent(spec) AgentFn<I,O>; spec={name,description,input,output,prompt,addons,maxTurns} |
43 | 46 | * F:copilotEngine(opts?) AgentFactory wrapping CopilotClient+RuntimeConnection |
@@ -67,7 +70,7 @@ import type { CopilotClientOptions } from "@github/copilot-sdk"; |
67 | 70 | export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; |
68 | 71 | export type ValidationResult = { ok: true } | { ok: false; error: string }; |
69 | 72 |
|
70 | | -export type StringSchema = { type: "string"; description?: string }; |
| 73 | +export type StringSchema = { type: "string"; description?: string; minLength?: number; format?: string }; |
71 | 74 | export type NumberSchema = { type: "number"; description?: string }; |
72 | 75 | export type IntegerSchema = { type: "integer"; description?: string }; |
73 | 76 | export type BooleanSchema = { type: "boolean"; description?: string }; |
@@ -142,6 +145,15 @@ function createTypedPrimitiveSchema<T extends StringSchema | NumberSchema | Inte |
142 | 145 | return factory; |
143 | 146 | } |
144 | 147 |
|
| 148 | +function createConstrainedStringSchema(constraint: Omit<StringSchema, "type" | "description">): SchemaHelperFactory<StringSchema> { |
| 149 | + const base = markAsSchema({ type: "string", ...constraint } as StringSchema); |
| 150 | + const factory = Object.assign( |
| 151 | + markAsSchema(((description?: string) => (description === undefined ? base : markAsSchema({ type: "string", ...constraint, description } as StringSchema))) as SchemaHelperFactory<StringSchema>), |
| 152 | + base, |
| 153 | + ); |
| 154 | + return factory; |
| 155 | +} |
| 156 | + |
145 | 157 | function createUnknownSchema(): SchemaHelperFactory<UnknownSchema> { |
146 | 158 | const base: UnknownSchema = markAsSchema({}); |
147 | 159 | const factory = Object.assign( |
@@ -198,10 +210,16 @@ export type InferSchema<T> = |
198 | 210 | export const s = { |
199 | 211 | /** Schema for a `string` value. Call as `s.string` or `s.string("description")`. */ |
200 | 212 | string: createTypedPrimitiveSchema<StringSchema>("string"), |
| 213 | + /** Schema for a non-empty `string` value (minLength: 1). Call as `s.nonEmptyString` or `s.nonEmptyString("description")`. */ |
| 214 | + nonEmptyString: createConstrainedStringSchema({ minLength: 1 }), |
| 215 | + /** Schema for a URL string (format: "uri"). Call as `s.url` or `s.url("description")`. */ |
| 216 | + url: createConstrainedStringSchema({ format: "uri" }), |
201 | 217 | /** Schema for a `number` value. Call as `s.number` or `s.number("description")`. */ |
202 | 218 | number: createTypedPrimitiveSchema<NumberSchema>("number"), |
203 | 219 | /** Schema for an integer value. Serializes to `{"type":"integer"}` in JSON Schema. Call as `s.integer` or `s.integer("description")`. */ |
204 | 220 | integer: createTypedPrimitiveSchema<IntegerSchema>("integer"), |
| 221 | + /** Schema for an integer value. Alias for `s.integer`. Call as `s.int` or `s.int("description")`. */ |
| 222 | + int: createTypedPrimitiveSchema<IntegerSchema>("integer"), |
205 | 223 | /** Schema for a `boolean` value. Call as `s.boolean` or `s.boolean("description")`. */ |
206 | 224 | boolean: createTypedPrimitiveSchema<BooleanSchema>("boolean"), |
207 | 225 | /** Schema for the JSON `null` literal. Call as `s.null` or `s.null("description")`. */ |
@@ -334,6 +352,13 @@ function serializeSchema(schema: Schema): JsonSchemaObject { |
334 | 352 | return withDescription(obj); |
335 | 353 | } |
336 | 354 | if ("type" in schema) { |
| 355 | + if (schema.type === "string") { |
| 356 | + const { minLength, format } = schema as StringSchema; |
| 357 | + const base: JsonSchemaObject = { type: "string" }; |
| 358 | + if (minLength !== undefined) base["minLength"] = minLength; |
| 359 | + if (format !== undefined) base["format"] = format; |
| 360 | + return withDescription(base); |
| 361 | + } |
337 | 362 | return withDescription({ type: schema.type }); |
338 | 363 | } |
339 | 364 | return withDescription({}); |
@@ -581,11 +606,12 @@ export type PromptIntentOptions = { |
581 | 606 | export type PromptIntent = { |
582 | 607 | __rig: "prompt"; |
583 | 608 | id: string; |
584 | | - mode: "prompt.text" | "prompt.read" | "prompt.write" | "prompt.glob"; |
| 609 | + mode: "prompt.text" | "prompt.read" | "prompt.write" | "prompt.glob" | "prompt.readOptional" | "prompt.env"; |
585 | 610 | command?: string; |
586 | 611 | path?: string; |
587 | 612 | contents?: string; |
588 | 613 | pattern?: string; |
| 614 | + fallback?: string; |
589 | 615 | options?: Omit<PromptIntentOptions, "signal">; |
590 | 616 | }; |
591 | 617 |
|
@@ -613,6 +639,28 @@ type PromptHelpers = { |
613 | 639 | * input: { source: p.read("src/index.ts") } |
614 | 640 | */ |
615 | 641 | read(path: string, options?: PromptIntentOptions): PromptIntent; |
| 642 | + /** |
| 643 | + * Declarative intent that instructs the LLM to read the file at `path` if it |
| 644 | + * exists and substitute its contents into the prompt. If the file does not |
| 645 | + * exist, the `fallback` string (default `""`) is used instead. The file is |
| 646 | + * **not** read in-process; resolution happens inside the Copilot runtime. |
| 647 | + * |
| 648 | + * @example |
| 649 | + * input: { config: p.readOptional(".eslintrc.json") } |
| 650 | + * input: { config: p.readOptional(".eslintrc.json", "{}") } |
| 651 | + */ |
| 652 | + readOptional(path: string, fallback?: string, options?: PromptIntentOptions): PromptIntent; |
| 653 | + /** |
| 654 | + * Declarative intent that instructs the LLM to read the environment variable |
| 655 | + * `name` and substitute its value into the prompt. If the variable is not |
| 656 | + * set, the `fallback` string (default `""`) is used instead. The variable is |
| 657 | + * **not** read in-process; resolution happens inside the Copilot runtime. |
| 658 | + * |
| 659 | + * @example |
| 660 | + * input: { token: p.env("GITHUB_TOKEN") } |
| 661 | + * input: { token: p.env("GITHUB_TOKEN", "unset") } |
| 662 | + */ |
| 663 | + env(name: string, fallback?: string, options?: PromptIntentOptions): PromptIntent; |
616 | 664 | /** |
617 | 665 | * Declarative intent that instructs the LLM to write `contents` to `path`. |
618 | 666 | * The write is **not** performed in-process; it is resolved by the Copilot |
@@ -754,6 +802,12 @@ export const p: PromptHelpers = Object.assign( |
754 | 802 | read(path: string, options?: PromptIntentOptions): PromptIntent { |
755 | 803 | return createPromptIntent("prompt.read", withOptions({ path }, options)); |
756 | 804 | }, |
| 805 | + readOptional(path: string, fallback = "", options?: PromptIntentOptions): PromptIntent { |
| 806 | + return createPromptIntent("prompt.readOptional", withOptions({ path, fallback }, options)); |
| 807 | + }, |
| 808 | + env(name: string, fallback = "", options?: PromptIntentOptions): PromptIntent { |
| 809 | + return createPromptIntent("prompt.env", withOptions({ command: name, fallback }, options)); |
| 810 | + }, |
757 | 811 | write(path: string, contents: string, options?: PromptIntentOptions): PromptIntent { |
758 | 812 | return createPromptIntent("prompt.write", withOptions({ path, contents }, options)); |
759 | 813 | }, |
@@ -1085,6 +1139,7 @@ async function runRootAgentFromStdin( |
1085 | 1139 | const resolvedPath = isAbsolute(programPath) ? programPath : resolve(cwd, programPath); |
1086 | 1140 | if (options.typecheck) { |
1087 | 1141 | await typecheckProgram(resolvedPath, cwd); |
| 1142 | + io.stdout.write("typecheck passed\n"); |
1088 | 1143 | return; |
1089 | 1144 | } |
1090 | 1145 |
|
@@ -1124,6 +1179,7 @@ async function runProgramCodeFromStdin( |
1124 | 1179 | try { |
1125 | 1180 | if (options.typecheck) { |
1126 | 1181 | await typecheckProgram(tempProgramPath, cwd, "<stdin>"); |
| 1182 | + io.stdout.write("typecheck passed\n"); |
1127 | 1183 | return; |
1128 | 1184 | } |
1129 | 1185 | configureAgent(copilotEngine(resolveCopilotOptions(cwd, options))); |
@@ -1602,7 +1658,17 @@ function validateSchema(value: unknown, schema: Schema, path: string, optional: |
1602 | 1658 | return ok(); |
1603 | 1659 | } |
1604 | 1660 | if ("type" in schema) { |
1605 | | - if (schema.type === "string") return typeof value === "string" ? ok() : bad(path, "string", value); |
| 1661 | + if (schema.type === "string") { |
| 1662 | + if (typeof value !== "string") return bad(path, "string", value); |
| 1663 | + const { minLength, format } = schema as StringSchema; |
| 1664 | + if (minLength !== undefined && value.length < minLength) { |
| 1665 | + return { ok: false, error: `${path}: expected string with minLength ${minLength}, got empty string` }; |
| 1666 | + } |
| 1667 | + if (format === "uri") { |
| 1668 | + try { new URL(value); } catch { return { ok: false, error: `${path}: expected a valid URL, got ${JSON.stringify(value)}` }; } |
| 1669 | + } |
| 1670 | + return ok(); |
| 1671 | + } |
1606 | 1672 | if (schema.type === "number") return typeof value === "number" ? ok() : bad(path, "number", value); |
1607 | 1673 | if (schema.type === "integer") return (typeof value === "number" && Number.isInteger(value)) ? ok() : bad(path, "integer", value); |
1608 | 1674 | if (schema.type === "boolean") return typeof value === "boolean" ? ok() : bad(path, "boolean", value); |
@@ -1684,6 +1750,10 @@ function renderPromptIntentInstruction(intent: PromptIntent): string { |
1684 | 1750 | return `Run bash command and return stdout as text: ${intent.command}${promptExecutionContext()}${options}`; |
1685 | 1751 | case "prompt.read": |
1686 | 1752 | return `Read file and return its contents as text: ${JSON.stringify(requiredPath(intent))}${promptExecutionContext()}${options}`; |
| 1753 | + case "prompt.readOptional": |
| 1754 | + return `Read file and return its contents as text: ${JSON.stringify(requiredPath(intent))}. If the file does not exist, return ${JSON.stringify(intent.fallback ?? "")} instead${promptExecutionContext()}${options}`; |
| 1755 | + case "prompt.env": |
| 1756 | + return `Read environment variable ${JSON.stringify(intent.command ?? "")} and return its value as text. If the variable is not set, return ${JSON.stringify(intent.fallback ?? "")} instead${promptExecutionContext()}${options}`; |
1687 | 1757 | case "prompt.write": |
1688 | 1758 | return `Write file at path ${JSON.stringify(requiredPath(intent))} with contents:\n${intent.contents ?? ""}${promptExecutionContext()}${options}`; |
1689 | 1759 | case "prompt.glob": |
|
0 commit comments