|
|
@@ -145,9 +145,47 @@ export const layer = Layer.effect(
|
|
|
yield* state.cancel(sessionID)
|
|
|
})
|
|
|
|
|
|
+ const resolveReferenceParts = Effect.fnUntraced(function* (template: string) {
|
|
|
+ const parts: Types.DeepMutable<PromptInput["parts"]> = []
|
|
|
+ const seen = new Set<string>()
|
|
|
+ yield* Effect.forEach(
|
|
|
+ ConfigMarkdown.files(template),
|
|
|
+ Effect.fnUntraced(function* (match) {
|
|
|
+ const name = match[1]
|
|
|
+ if (!name) return
|
|
|
+ const alias = name.split("/")[0]
|
|
|
+ if (!alias || seen.has(alias)) return
|
|
|
+ const reference = yield* references.get(alias)
|
|
|
+ if (!reference) return
|
|
|
+ seen.add(alias)
|
|
|
+
|
|
|
+ const start = match.index ?? 0
|
|
|
+ const source = { value: match[0], start, end: start + match[0].length }
|
|
|
+ if (reference.kind === "invalid") {
|
|
|
+ parts.push(referenceTextPart({ reference, source }))
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ yield* references.ensure(reference.path)
|
|
|
+ parts.push({
|
|
|
+ type: "file",
|
|
|
+ url: pathToFileURL(reference.path).href,
|
|
|
+ filename: alias,
|
|
|
+ mime: "application/x-directory",
|
|
|
+ source: { type: "file", text: source, path: alias },
|
|
|
+ })
|
|
|
+ }),
|
|
|
+ { concurrency: 1, discard: true },
|
|
|
+ )
|
|
|
+ return parts
|
|
|
+ })
|
|
|
+
|
|
|
const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) {
|
|
|
const ctx = yield* InstanceState.context
|
|
|
- const parts: Types.DeepMutable<PromptInput["parts"]> = [{ type: "text", text: template }]
|
|
|
+ const parts: Types.DeepMutable<PromptInput["parts"]> = [
|
|
|
+ { type: "text", text: template },
|
|
|
+ ...(yield* resolveReferenceParts(template)),
|
|
|
+ ]
|
|
|
const files = ConfigMarkdown.files(template)
|
|
|
const seen = new Set<string>()
|
|
|
yield* Effect.forEach(
|
|
|
@@ -160,60 +198,7 @@ export const layer = Layer.effect(
|
|
|
|
|
|
const slash = name.indexOf("/")
|
|
|
const alias = slash === -1 ? name : name.slice(0, slash)
|
|
|
- const reference = yield* references.get(alias)
|
|
|
- if (reference) {
|
|
|
- const start = match.index ?? 0
|
|
|
- const source = { value: match[0], start, end: start + match[0].length }
|
|
|
- if (reference.kind === "invalid") {
|
|
|
- parts.push(
|
|
|
- referenceTextPart({ reference, source, target: slash === -1 ? undefined : name.slice(slash + 1) }),
|
|
|
- )
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- yield* references.ensure(reference.path)
|
|
|
- if (slash === -1) {
|
|
|
- parts.push(referenceTextPart({ reference, source }))
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const target = name.slice(slash + 1)
|
|
|
- const targetPath = path.resolve(reference.path, target)
|
|
|
- if (!FSUtil.contains(reference.path, targetPath)) {
|
|
|
- parts.push(
|
|
|
- referenceTextPart({
|
|
|
- reference,
|
|
|
- source,
|
|
|
- target,
|
|
|
- targetPath,
|
|
|
- problem: `Path escapes configured reference @${alias}: ${target}`,
|
|
|
- }),
|
|
|
- )
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const info = yield* fsys.stat(targetPath).pipe(Effect.option)
|
|
|
- if (Option.isNone(info)) {
|
|
|
- parts.push(
|
|
|
- referenceTextPart({
|
|
|
- reference,
|
|
|
- source,
|
|
|
- target,
|
|
|
- targetPath,
|
|
|
- problem: `Path does not exist inside configured reference @${alias}: ${target}`,
|
|
|
- }),
|
|
|
- )
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- parts.push({
|
|
|
- type: "file",
|
|
|
- url: pathToFileURL(targetPath).href,
|
|
|
- filename: name,
|
|
|
- mime: info.value.type === "Directory" ? "application/x-directory" : "text/plain",
|
|
|
- })
|
|
|
- return
|
|
|
- }
|
|
|
+ if (yield* references.get(alias)) return
|
|
|
|
|
|
const filepath = name.startsWith("~/")
|
|
|
? path.join(os.homedir(), name.slice(2))
|
|
|
@@ -770,30 +755,6 @@ export const layer = Layer.effect(
|
|
|
id: part.id ? PartID.make(part.id) : PartID.ascending(),
|
|
|
})
|
|
|
|
|
|
- const referenceContextFromFilePart = Effect.fnUntraced(function* (
|
|
|
- part: Extract<PromptInput["parts"][number], { type: "file" }>,
|
|
|
- filepath: string,
|
|
|
- ) {
|
|
|
- const name = part.filename?.replace(/#\d+(?:-\d*)?$/, "")
|
|
|
- if (!name) return
|
|
|
- const slash = name.indexOf("/")
|
|
|
- if (slash === -1) return
|
|
|
-
|
|
|
- const reference = yield* references.get(name.slice(0, slash))
|
|
|
- if (!reference || reference.kind === "invalid") return
|
|
|
- if (!FSUtil.contains(reference.path, filepath)) return
|
|
|
-
|
|
|
- const target = path.relative(reference.path, filepath).split(path.sep).join("/")
|
|
|
- if (!target || target.startsWith("../") || target === "..") return
|
|
|
-
|
|
|
- return referenceTextPart({
|
|
|
- reference,
|
|
|
- source: part.source?.text ?? { value: `@${name}`, start: 0, end: name.length + 1 },
|
|
|
- target,
|
|
|
- targetPath: filepath,
|
|
|
- })
|
|
|
- })
|
|
|
-
|
|
|
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<SessionV1.Part>[]> = Effect.fn(
|
|
|
"SessionPrompt.resolveUserPart",
|
|
|
)(function* (part) {
|
|
|
@@ -876,7 +837,6 @@ export const layer = Layer.effect(
|
|
|
case "file:": {
|
|
|
log.info("file", { mime: part.mime })
|
|
|
const filepath = fileURLToPath(part.url)
|
|
|
- const referenceContext = yield* referenceContextFromFilePart(part, filepath)
|
|
|
const mime = (yield* fsys.isDir(filepath)) ? "application/x-directory" : part.mime
|
|
|
|
|
|
const { read } = yield* registry.named()
|
|
|
@@ -922,9 +882,6 @@ export const layer = Layer.effect(
|
|
|
}
|
|
|
const args = { filePath: filepath, offset, limit }
|
|
|
const pieces: Draft<SessionV1.Part>[] = [
|
|
|
- ...(referenceContext
|
|
|
- ? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
|
|
- : []),
|
|
|
{
|
|
|
messageID: info.id,
|
|
|
sessionID: input.sessionID,
|
|
|
@@ -990,9 +947,6 @@ export const layer = Layer.effect(
|
|
|
error: new NamedError.Unknown({ message }).toObject(),
|
|
|
})
|
|
|
return [
|
|
|
- ...(referenceContext
|
|
|
- ? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
|
|
- : []),
|
|
|
{
|
|
|
messageID: info.id,
|
|
|
sessionID: input.sessionID,
|
|
|
@@ -1003,9 +957,6 @@ export const layer = Layer.effect(
|
|
|
]
|
|
|
}
|
|
|
return [
|
|
|
- ...(referenceContext
|
|
|
- ? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }]
|
|
|
- : []),
|
|
|
{
|
|
|
messageID: info.id,
|
|
|
sessionID: input.sessionID,
|
|
|
@@ -1025,7 +976,6 @@ export const layer = Layer.effect(
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
- ...(referenceContext ? [{ ...referenceContext, messageID: info.id, sessionID: input.sessionID }] : []),
|
|
|
{
|
|
|
messageID: info.id,
|
|
|
sessionID: input.sessionID,
|
|
|
@@ -1071,7 +1021,22 @@ export const layer = Layer.effect(
|
|
|
return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
|
|
|
})
|
|
|
|
|
|
- const resolvedParts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe(
|
|
|
+ const submittedParts: Types.DeepMutable<PromptInput["parts"]> = [...input.parts]
|
|
|
+ const attachedReferences = new Set(
|
|
|
+ input.parts.flatMap((part) =>
|
|
|
+ part.type === "file" && part.mime === "application/x-directory" ? [part.url] : [],
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ for (const part of input.parts) {
|
|
|
+ if (part.type !== "text" || part.synthetic) continue
|
|
|
+ for (const reference of yield* resolveReferenceParts(part.text)) {
|
|
|
+ if (reference.type === "file" && attachedReferences.has(reference.url)) continue
|
|
|
+ if (reference.type === "file") attachedReferences.add(reference.url)
|
|
|
+ submittedParts.push(reference)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const resolvedParts = yield* Effect.forEach(submittedParts, resolvePart, { concurrency: "unbounded" }).pipe(
|
|
|
Effect.map((x) => x.flat().map(assign)),
|
|
|
)
|
|
|
|