{"id":30853524,"url":"https://github.com/grahms/promptweaver","last_synced_at":"2025-09-07T09:35:17.681Z","repository":{"id":310125746,"uuid":"1037501387","full_name":"GraHms/promptweaver","owner":"GraHms","description":"PromptWeaver is a Go library for progressively parsing and structuring hybrid text streams—text that blends plain narrative, XML-like tags, and metadata—into discrete, actionable events. It is designed to handle LLM outputs, agent instructions, developer logs, or any mixed-content stream in real time.","archived":false,"fork":false,"pushed_at":"2025-08-18T16:00:28.000Z","size":56,"stargazers_count":5,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"master","last_synced_at":"2025-09-01T22:53:37.826Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GraHms.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-13T17:05:34.000Z","updated_at":"2025-08-18T16:00:32.000Z","dependencies_parsed_at":"2025-08-15T23:36:50.805Z","dependency_job_id":"f0b8287d-000a-47f3-a401-1f7341517dab","html_url":"https://github.com/GraHms/promptweaver","commit_stats":null,"previous_names":["grahms/promptweaver"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/GraHms/promptweaver","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GraHms%2Fpromptweaver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GraHms%2Fpromptweaver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GraHms%2Fpromptweaver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GraHms%2Fpromptweaver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GraHms","download_url":"https://codeload.github.com/GraHms/promptweaver/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GraHms%2Fpromptweaver/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":274021044,"owners_count":25208729,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-07T02:00:09.463Z","response_time":67,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-09-07T09:35:16.063Z","updated_at":"2025-09-07T09:35:17.664Z","avatar_url":"https://github.com/GraHms.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Promptweaver\n\n**Promptweaver** is a streaming, XML-lite **event parser** for model output.\nYou give it an `io.Reader`. You register a handful of tags.\nIt emits **events** the moment a tag closes.\n\nNo DOM. No schema. No nesting rules to memorize.\nJust a clean, predictable way to turn text into actions.\n\n---\n\n## Explainer\n\nLarge models often speak in paragraphs but **act** in sections:\n\n```xml\n\u003cthink\u003eOutline a plan…\u003c/think\u003e\n\u003ccreate-file path=\"app/page.tsx\"\u003e…code…\u003c/create-file\u003e\n\u003csummary\u003eWhat changed and why.\u003c/summary\u003e\n```\n\nYou don’t want to wait for the whole response. You want to **react as soon as a section is done**:\n\n* print the plan,\n* write the file,\n* then show the summary.\n\nPromptweaver reads the stream as it arrives and emits an event on each **closing tag**. Everything between `\u003copen …\u003e` and its matching `\u003c/close\u003e` is delivered verbatim (for recognized tags). That keeps code intact—JSX, angle brackets, braces—without tripping a structured parser.\n\n---\n\n## What it does\n\n* Reads bytes incrementally from an `io.Reader`.\n* Recognizes **section tags** you register (with **aliases**).\n* For each recognized tag:\n\n    * Captures **all content** between `\u003ctag …\u003e` and the **first** matching `\u003c/tag\u003e`.\n    * Emits a `SectionEvent` with `Name`, `Attrs`, and `Content`.\n\nThat’s the contract.\n\n---\n\n## What it does **not** do\n\n* It does **not** parse nested sections inside a recognized section.\n  Once a section opens, **everything** until its closer is treated as plain text.\n* It does **not** invent closes.\n  If a section never closes, it runs to **EOF**; then Promptweaver emits what it has.\n* It does **not** spawn goroutines or manage I/O for you.\n  Your handlers are called synchronously.\n\n---\n\n## Installation\n\n```bash\ngo get github.com/grahms/promptweaver\n```\n\n\nGo 1.21+ recommended.\n\n---\n\n## Quick Start\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/yourorg/promptweaver\"\n)\n\nfunc main() {\n\t// 1) Declare the tags your model will use\n\treg := promptweaver.NewRegistry()\n\treg.Register(promptweaver.SectionPlugin{Name: \"think\"})\n\treg.Register(promptweaver.SectionPlugin{\n\t\tName:    \"write-file\",\n\t\tAliases: []string{\"create-file\", \"dyad-write\"}, // any spelling the model might use\n\t})\n\treg.Register(promptweaver.SectionPlugin{Name: \"summary\"})\n\n\t// 2) Wire handlers for canonical names\n\tsink := promptweaver.NewHandlerSink()\n\tsink.RegisterHandler(\"think\", func(ev promptweaver.SectionEvent) {\n\t\tfmt.Println(\"[THINK]\\n\" + strings.TrimSpace(ev.Content))\n\t})\n\n\tbase := mustAbs(\"./workspace\")\n\tsink.RegisterHandler(\"write-file\", func(ev promptweaver.SectionEvent) {\n\t\tout := secureJoin(base, ev.Attrs[\"path\"])\n\t\t_ = os.MkdirAll(filepath.Dir(out), 0o755)\n\t\tif err := os.WriteFile(out, []byte(ev.Content), 0o644); err != nil {\n\t\t\tfmt.Println(\"write error:\", err)\n\t\t\treturn\n\t\t}\n\t\tfmt.Printf(\"[CREATE FILE] %s ok\\n\", out)\n\t})\n\n\tsink.RegisterHandler(\"summary\", func(ev promptweaver.SectionEvent) {\n\t\tfmt.Println(\"[SUMMARY]\\n\" + strings.TrimSpace(ev.Content))\n\t})\n\n\t// 3) Stream\n\tengine := promptweaver.NewEngine(reg)\n\tsrc := promptweaver.ReaderFromString(\n\t\t`\u003cthink\u003eplan\u003c/think\u003e` +\n\t\t\t`\u003ccreate-file path=\"main.ts\"\u003econsole.log(\"hi\")\u003c/create-file\u003e` +\n\t\t\t`\u003csummary\u003edone\u003c/summary\u003e`,\n\t)\n\n\tif err := engine.ProcessStream(src, sink); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc mustAbs(p string) string { a, _ := filepath.Abs(p); return a }\n\n// deny writes outside base\nfunc secureJoin(base, rel string) string {\n\tbase = filepath.Clean(base)\n\tpath := filepath.Clean(filepath.Join(base, rel))\n\tif path != base \u0026\u0026 !strings.HasPrefix(path, base+string(os.PathSeparator)) {\n\t\tpanic(\"refusing to write outside base: \" + rel)\n\t}\n\treturn path\n}\n```\n\n---\n\n## API\n\n```go\ntype SectionPlugin struct {\n\tName    string   // canonical section name (what handlers see)\n\tAliases []string // alternative spellings your model might use\n}\n\ntype SectionEvent struct {\n\tName    string            // canonical name\n\tAttrs   map[string]string // attribute keys are lowercased\n\tContent string            // everything between \u003copen\u003e and \u003c/close\u003e\n}\n\n// Registry maps aliases -\u003e canonical\nreg := promptweaver.NewRegistry()\nreg.Register(promptweaver.SectionPlugin{Name: \"think\"})\nreg.Register(promptweaver.SectionPlugin{Name: \"write-file\", Aliases: []string{\"create-file\"}})\n\n// Handlers route by canonical name\nsink := promptweaver.NewHandlerSink()\nsink.RegisterHandler(\"write-file\", func(ev promptweaver.SectionEvent) { /* ... */ })\n\nengine := promptweaver.NewEngine(reg)\n_ = engine.ProcessStream(reader, sink)\n```\n\n---\n\n## Streaming Semantics\n\n* **Emit on close**: an event fires as soon as `\u003c/tag\u003e` is read. No need to buffer the whole response.\n* **Flat model**: inside a recognized section, Promptweaver **does not** parse inner tags; it treats them as content. This is why code survives intact.\n* **Unknown tags**:\n\n    * outside any recognized section: ignored.\n    * inside a recognized section: treated as literal text.\n* **EOF**: if the stream ends with a recognized section still open, that section is emitted with whatever content arrived.\n\n---\n\n## Tag Grammar\n\nPromptweaver accepts a small, well-defined subset:\n\n* **Open**\n\n  ```\n  \u003cname a=\"x\" b='y' c={expr}\u003e\n  ```\n\n    * `name`: letters, digits, `_`, `-`; case-insensitive.\n    * Attributes:\n\n        * keys are lowercased.\n        * values can be:\n\n            * `\"double-quoted\"`\n            * `'single-quoted'`\n            * `{ … }` (JSX-style). Braces are balanced; quotes inside are skipped.\n\n* **Close**\n\n  ```\n  \u003c/name\u003e\n  \u003c/   name   \u003e\n  ```\n\n    * Spaces after `\u003c/` and before `\u003e` are allowed.\n    * Name matching is **case-insensitive**.\n\n* **Self-closing**\n\n  ```\n  \u003cname …/\u003e\n  ```\n\n    * If `name` is recognized, an event is emitted with empty content.\n\n* **Aliases**\n\n    * Open with `\u003ccreate-file\u003e` and close with `\u003c/dyad-write\u003e` if both alias to the same canonical (e.g., `write-file`).\n    * If a closer name isn’t in the alias map, Promptweaver falls back to a **literal** match with the original open name.\n\n---\n\n## Practical Recipes\n\n### Multi-file code generation\n\nModel output:\n\n```xml\n\u003cthink\u003eOutline\u003c/think\u003e\n\u003ccreate-file path=\"app/page.tsx\"\u003e…\u003c/create-file\u003e\n\u003ccreate-file path=\"lib/api.ts\"\u003e…\u003c/create-file\u003e\n\u003csummary\u003eNotes and next steps.\u003c/summary\u003e\n```\n\nHandlers:\n\n* `think` → print to console.\n* `write-file` → write to a sandboxed workspace; run a linter per file if you like.\n* `summary` → display as a final report.\n\n### Tool calls without JSON\n\n```xml\n\u003cthink\u003ePlan\u003c/think\u003e\n\u003crun-bash cwd=\".\" timeout=\"30s\"\u003enpm test\u003c/run-bash\u003e\n\u003csummary\u003eWhat failed and why.\u003c/summary\u003e\n```\n\nRegister `run-bash` and gate the handler with your policies. Since it is a recognized tag, the command body is captured exactly as written.\n\n### Incremental extraction\n\n```xml\n\u003crecord id=\"1\"\u003e…\u003c/record\u003e\n\u003crecord id=\"2\"\u003e…\u003c/record\u003e\n\u003crecord id=\"3\"\u003e…\u003c/record\u003e\n```\n\nEach record arrives as soon as it closes. You can ingest them one by one.\n\n---\n\n## Debugging\n\n* **See the exact text the model sent**\n\n  ```go\n  tee := io.TeeReader(reader, os.Stdout)\n  _ = engine.ProcessStream(tee, sink)\n  ```\n\n* **Test with small chunks**\n\n  ```go\n  type chunkedReader struct{ data []byte; pos, chunk int }\n  func (c *chunkedReader) Read(p []byte) (int, error) {\n  \tif c.pos \u003e= len(c.data) { return 0, io.EOF }\n  \tn := c.chunk\n  \tif n \u003e len(c.data)-c.pos { n = len(c.data)-c.pos }\n  \tcopy(p, c.data[c.pos:c.pos+n])\n  \tc.pos += n\n  \treturn n, nil\n  }\n  ```\n\nFeed the engine with `chunk=32` to exercise the tokenizer.\n\n---\n\n## Security Notes\n\n* Treat attributes as untrusted input. If you write files, **sanitize paths** and fence them under a base directory (see `secureJoin` in the Quick Start).\n* Apply allow-lists in handlers (`path` prefixes, URL hosts, command names) as needed by your environment.\n\n---\n\n## FAQ\n\n**Why not JSON?**\nBecause code and prose contain braces and commas. JSON is brittle under truncation and edits. Tags degrade more gracefully and are easier to repair mentally.\n\n**Can I nest sections?**\nNo. That is a deliberate constraint. Inside a recognized section, everything is text until the matching close. If you need true nesting, build a different layer on top.\n\n**What happens if the model never closes a tag?**\nThe section emits at EOF with whatever content arrived. Fix the prompt to include the closer.\n\n**Do attribute keys keep their case?**\nThey’re lowercased in the event. Values are returned without quotes (and with braces preserved for `{…}`).\n\n**Can a closer include spaces?**\nYes: `\u003c/   create-file   \u003e` is accepted.\n\n---\n\n## Design Notes\n\n* **Flat, one-section state.** This keeps the rules simple and the behavior predictable. Code inside sections won’t collide with the parser.\n* **Alias mapping.** The model can vary tag names; your handlers see only canonical names.\n* **Tokenizer tolerance.** Whitespace, quoted strings, and JSX braces are handled in a way that follows the stream without guessing.\n\nShort code. Clear rules. Immediate effects.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrahms%2Fpromptweaver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrahms%2Fpromptweaver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrahms%2Fpromptweaver/lists"}