Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
248 commits
Select commit Hold shift + click to select a range
0b03a7e
feat(replicate): add complete Replicate provider with all core features
cemarta7 Nov 22, 2025
ded1505
feat: Add an interface for the ability to process speech-to-text asyn…
Dec 2, 2025
4444025
add azure to prism
Matildevoldsen Jan 16, 2026
d81de06
Add Vertex support
machour Feb 5, 2026
3fb1bad
feat: add Qwen (Alibaba Cloud DashScope) provider
leslieeilsel Feb 10, 2026
b98d832
fix(streaming): add null usage fallback to all providers
aimbit-ni Feb 11, 2026
59d0875
feat(openai): add chat/completions api_format config option
aimbit-ni Feb 11, 2026
2edd784
fix(openai): return null from ChatCompletionsToolMap when no tools co…
aimbit-ni Feb 12, 2026
1a14ebe
feat(openai): support citations in chat/completions streaming
aimbit-ni Feb 12, 2026
ec19732
fix azure streaming
Matildevoldsen Mar 1, 2026
d29c47f
track cost in usage for providers like openrouter who return cost in …
vinitkadam03 Feb 22, 2026
654d906
Merge branch 'main' into feat/add-qwen-provider
leslieeilsel Mar 5, 2026
8c3b47a
feat(batch): implement batch processing for Anthropic provider with j…
joao-salomao Mar 14, 2026
b9256d8
feat(batch): add batch processing capabilities to OpenAI provider wit…
joao-salomao Mar 14, 2026
90a5a9d
Merge remote-tracking branch 'origin/main' into feature/batch-api-for…
joao-salomao Mar 14, 2026
9c4b69c
test(batch): split handlers and test them
joao-salomao Mar 14, 2026
f9a87d0
feat(provider): enhance providerOptions method to accept a default va…
joao-salomao Mar 14, 2026
4f30a6c
feat(batch): introduce new batch request classes and enhance existing…
joao-salomao Mar 14, 2026
6efb5bd
feat(files): add file management capabilities with upload, download, …
joao-salomao Mar 15, 2026
d64aacb
feat(batch): add Batch Processing and Files API documentation, enhanc…
joao-salomao Mar 15, 2026
27d1ead
fix(phpstan): resolve static analysis errors in Create handler and Te…
joao-salomao Mar 15, 2026
37f7291
test: update response assertions to decode JSON structure
joao-salomao Mar 15, 2026
8f02f2b
feat(batch): enhance BatchJob to include error handling and map error…
joao-salomao Mar 16, 2026
6d902aa
feat(batch): improve error handling in OpenAI results processing and …
joao-salomao Mar 16, 2026
916c9bc
feat(xai): Add XAI image generation handler and tests
timothymarois Mar 17, 2026
dc9f364
Merge branch 'main' into vertex-support
machour Mar 18, 2026
e63bace
Initial plan
Copilot Mar 24, 2026
1a69f73
Fix global region hostname in Vertex buildBaseUrl
Copilot Mar 24, 2026
5dd3c3d
Merge pull request #2 from machour/copilot/sub-pr-1
machour Mar 24, 2026
123be2b
feat(anthropic): add fine-grained tool streaming support
alexlampdeveloper Mar 26, 2026
4162e61
feat(fim): add FIM request and response handling for Mistral AI
mssayari Apr 15, 2026
e9ec698
feat: add image generation support to Azure provider
Matildevoldsen Apr 19, 2026
7049568
feat: add provider-agnostic withReasoning() request method
ChrisThompsonTLDR May 2, 2026
2a41544
Add Requesty as a provider
Thibaultjaigu Jul 4, 2026
e2951ae
Merge prism-php/prism#988 — feat(anthropic): add fine-grained tool st…
wishborn Jul 6, 2026
e396541
Merge prism-php/prism#998 — feat(anthropic): adaptive thinking and ou…
wishborn Jul 6, 2026
9defb79
Merge prism-php/prism#1018 — feat: add provider-agnostic withReasonin…
wishborn Jul 6, 2026
397a7b3
fix(anthropic): honor withReasoning(false) in resolveThinking
wishborn Jul 6, 2026
b9a82d4
Merge prism-php/prism#1014 — feat(mistral): add FIM (fill-in-the-midd…
wishborn Jul 6, 2026
dfbaf8e
Merge prism-php/prism#960 — feat(xai): add image generation handler
wishborn Jul 6, 2026
4b37140
Merge prism-php/prism#978 — feat(vertex): add Google Vertex AI provider
wishborn Jul 6, 2026
b20cefc
Merge prism-php/prism#1026 — feat(requesty): add Requesty provider
wishborn Jul 6, 2026
fee9572
fix(requesty): align new provider with fork conventions + backfill tests
wishborn Jul 6, 2026
fa3a886
fix(vertex): repair ADC credentials fallback and type safety
wishborn Jul 6, 2026
8f2003b
Merge prism-php/prism#951 — feat: batching and files APIs for Anthrop…
wishborn Jul 6, 2026
7cfe23c
fix(batch): repair merge fallout and make Anthropic payload guard tes…
wishborn Jul 6, 2026
c86c440
style: run pint + rector over merged Batch 3 changes
wishborn Jul 6, 2026
23b204a
Merge prism-php/prism#810 — feat(audio): add async speech-to-text int…
wishborn Jul 6, 2026
13a3dff
Merge prism-php/prism#920 — feat: track cost in Usage for providers t…
wishborn Jul 6, 2026
371dcd7
Merge prism-php/prism#907 — feat(openai): chat/completions api_format…
wishborn Jul 6, 2026
a091039
Merge prism-php/prism#835 — feat(azure): add Azure OpenAI provider wi…
wishborn Jul 6, 2026
cf46838
fix(azure): use protected visibility per arch rules (prism-php/prism#…
wishborn Jul 6, 2026
5b77f72
Merge prism-php/prism#898 — feat(qwen): add Qwen (Alibaba Cloud DashS…
wishborn Jul 6, 2026
dc92c10
Merge prism-php/prism#757 — feat(replicate): add Replicate provider
wishborn Jul 6, 2026
7b24105
fix(replicate): pass providerToolCalls to Step (prism-php/prism#757 f…
wishborn Jul 6, 2026
1d0d2c0
fix: resolve PHPStan errors in Azure, Qwen and Replicate providers
wishborn Jul 6, 2026
6c1c6c0
feat(tool): coerce string arguments to declared scalar and BackedEnum…
wishborn Jul 6, 2026
9cad60f
feat(anthropic): support per-request beta features via anthropic_beta…
wishborn Jul 6, 2026
d1ffd4a
test(groq): cover Llama function-name splitting end-to-end (issue #3 …
wishborn Jul 6, 2026
47f4720
style: run pint + rector over Batch 4 changes
wishborn Jul 6, 2026
c85da4b
feat: client-executed tools and human-in-the-loop approval flow
wishborn Jul 7, 2026
7b48fa6
fix(replicate): harden generated-image auto-download (closes #5)
wishborn Jul 7, 2026
7d9793a
style: pint + rector over Batch 5 changes
wishborn Jul 7, 2026
a50139a
feat: roll out client-executed tools + approval flow to all providers
wishborn Jul 7, 2026
a959531
feat: emit HITL events in Anthropic and OpenAI streams
wishborn Jul 7, 2026
b44a7c2
docs: human-in-the-loop guide (client tools + approval flow)
wishborn Jul 7, 2026
a9a3549
style: pint + rector over phase 2 changes
wishborn Jul 7, 2026
7fe66a7
feat: HITL streaming for all remaining provider streams
wishborn Jul 7, 2026
4d20868
test(mistral): backfill fixture tests for structured tool-call loop (…
wishborn Jul 7, 2026
9d595e9
test(openrouter): re-add streaming payload test for parameterless too…
wishborn Jul 7, 2026
5b6af16
Merge issue-3-followups — test backfills for Mistral structured tools…
wishborn Jul 7, 2026
805baab
fix: exclude cached tokens from promptTokens in OpenRouter, Vertex an…
wishborn Jul 7, 2026
5f0bd4f
fix: report cached tokens and stop double counting across seven provi…
wishborn Jul 7, 2026
929322a
chore: declare real dependencies and gate them with composer-require-…
wishborn Jul 7, 2026
dccfa9a
fix: streaming buffering (DeepSeek/Qwen), malformed tool-call JSON, O…
wishborn Jul 7, 2026
183fde5
docs: document Usage token semantics and the completionTokens inconsi…
wishborn Jul 7, 2026
a5cc5fb
feat(vertex): added support for multi region endpoints
tognee Jul 9, 2026
864f599
Merge pull request #6 from infofactory/feat/add-multi-region-endpoint…
wishborn Jul 9, 2026
6474482
feat(telemetry): add neutral event layer (context, stack, events)
wishborn Jul 13, 2026
ad6c1fe
feat(telemetry): emit lifecycle, tool, and stream events from generat…
wishborn Jul 13, 2026
7b51a6a
fix(telemetry): gate tool call/result content behind capture_content
wishborn Jul 13, 2026
0301fdc
style: apply rector to telemetry test (null check -> exclusive instan…
wishborn Jul 14, 2026
3402e74
fix: polyfill json_validate() for PHP 8.2 support
wishborn Jul 14, 2026
ac05603
Merge pull request #9 from Particle-Academy/fix/php83-polyfill
wishborn Jul 14, 2026
d73408d
Merge remote-tracking branch 'origin/main' into feat/telemetry-935
wishborn Jul 14, 2026
66fb579
docs: point README to the ai.particle.academy docs site
wishborn Jul 14, 2026
cc773fb
Merge pull request #11 from Particle-Academy/docs/readme-docs-url
wishborn Jul 14, 2026
dfb1efe
feat(telemetry): tag tool events with their owning step
wishborn Jul 16, 2026
e84fb70
feat(telemetry): enrich trace context and captured stream content
wishborn Jul 17, 2026
b26213a
Merge #10: evented telemetry (#935) + step-tagged tool events + bound…
wishborn Jul 18, 2026
ec1527c
chore: add CODEOWNERS
wishborn Jul 25, 2026
a1a7858
fix(gemini,vertex): always send `tools` as a JSON array (#12)
saimaz Aug 5, 2026
2a36f0e
test(gemini,vertex): cover tools-array shape across handlers (#13)
wishborn Aug 5, 2026
fb386a5
docs: add Telemetry guide
wishborn Aug 6, 2026
1e2478b
chore(docs): refresh lockfile to clear dev-dependency advisories
wishborn Aug 9, 2026
31888e5
Normalise line endings to LF and trim non-runtime docs from the dist
wishborn Aug 11, 2026
182ffea
fix: stop discarding content that is exactly "0"
wishborn Aug 12, 2026
9029361
Allow Vertex to combine provider tools with custom function tools
wishborn Aug 12, 2026
92e0e79
Absorb prism-php/prism#1030 — map video containers to audio extensions
wishborn Aug 12, 2026
d5695a2
docs(openai): document OpenAI-compatible endpoints
wishborn Aug 12, 2026
0f4e8ae
fix: stop array_filter() dropping message content that is exactly "0"…
wishborn Aug 12, 2026
dbb48bb
Declare that this fork replaces prism-php/prism (#21)
wishborn Aug 20, 2026
32c1938
Add a Thread contract so stored conversations can supply history
wishborn Aug 23, 2026
ab8fad3
Refuse Prism tools on Perplexity instead of silently ignoring them
wishborn Aug 25, 2026
7125227
Stop the DeepSeek docs recommending a withdrawn model
wishborn Aug 25, 2026
cba99c5
Report the cost Perplexity already tells us (#26)
wishborn Aug 26, 2026
b13ec57
Stop dropping a prompt that is exactly "0" (#27)
wishborn Aug 26, 2026
4e83207
Make Embedding::fromArray compose with toArray (#28)
wishborn Aug 26, 2026
ae6687c
Ship an agent guide with the package
wishborn Aug 26, 2026
fe8e3cd
Track the agent guide instead of ignoring it
wishborn Aug 26, 2026
20c174c
Add make:prism-tool
wishborn Aug 26, 2026
9d4879b
Document the companion packages on the doc site
wishborn Aug 26, 2026
cfafdee
Stop documenting a Schema class that has never existed
wishborn Aug 27, 2026
4cef77d
Declare symfony/console
wishborn Aug 27, 2026
a38a85a
feat(perplexity): preserve agent controls and metadata
wishborn Aug 30, 2026
fe43f68
fix(perplexity): stop sending Sonar-era fields the Agent API rejects
wishborn Sep 1, 2026
e74bb15
fix(perplexity): stop the refusals from firing on a value nobody aske…
wishborn Sep 2, 2026
e422066
style: split the refusal guard into two continues, per Rector
wishborn Sep 2, 2026
a746657
Merge #32: stop sending Sonar-era fields the Agent API rejects (#31)
wishborn Sep 2, 2026
f8fd963
Add a release workflow that proves the tag is installable
wishborn Sep 4, 2026
7825296
fix: send map-typed fields as JSON objects, and stop guessing which t…
wishborn Sep 5, 2026
6ef227b
An unreadable rate-limit header must not destroy the response it desc…
wishborn Sep 5, 2026
02c3232
A title-casing proxy must not erase every rate limit
wishborn Sep 5, 2026
2a7ab5c
Pin a formatter floor, so the Formatting gate is reproducible
wishborn Sep 5, 2026
4ee5f95
Pin the formatter to a version the PHP floor can actually install
wishborn Sep 5, 2026
f5237ed
Publish the tag's own notes, not a list of commit subjects
wishborn Sep 5, 2026
d111c73
Carry provider rate limits on the telemetry events, not on the gated …
wishborn Sep 5, 2026
baa52e6
Write the tool maps the way the Formatting gate wants them
wishborn Sep 5, 2026
83a30a3
Read the tag annotation instead of trusting a flag that silently subs…
wishborn Sep 5, 2026
7ae75ac
Read the release notes from the API, after two mechanisms failed sile…
wishborn Sep 5, 2026
1e3c91f
Report the thinking tokens Anthropic sends (fixes #33)
wishborn Sep 5, 2026
1dcbbcb
Put the batch named arguments back in declaration order
wishborn Sep 5, 2026
f9fc415
Stop reporting citations on Anthropic responses that cite nothing (G-49)
wishborn Sep 5, 2026
f36182f
Let a caller reach Anthropic context management, and see what it did …
wishborn Sep 7, 2026
3339966
Lock in that a per-request anthropic_beta STRING merges rather than r…
wishborn Sep 7, 2026
4d02b31
Document context management, and where the counts actually live
wishborn Sep 7, 2026
069f0b9
Map OpenAI 503 to overloaded, like every other provider already did
wishborn Sep 10, 2026
2470679
Make hasBase64() answer the question its name asks
wishborn Sep 10, 2026
e612864
Hand back the image a hosted image tool generated
wishborn Sep 11, 2026
1107317
Patch js-yaml to 3.15.2 in the docs build, closing Dependabot #31
wishborn Sep 13, 2026
b85fdd2
Stop dereferencing media URLs server-side
wishborn Sep 14, 2026
2a25a94
Apply Rector to the Replicate regression test
wishborn Sep 14, 2026
220a4d8
Drop four dev dependencies nothing in the package uses
wishborn Sep 14, 2026
92f2a41
Run the org third-party allowlist check in CI
wishborn Sep 14, 2026
3b43b8b
Store media the way both ports do: with its bytes, its kind, and no p…
wishborn Sep 14, 2026
4ab495d
Say what the allowlist check in the release guard can actually stop
wishborn Sep 14, 2026
5b687d8
Withhold attachment bytes from captured telemetry unless capture_medi…
wishborn Sep 14, 2026
f63cde0
Read the default thinking budget from the key the config file defines
wishborn Sep 14, 2026
1ce65d8
Send a thinking option Prism does not spell itself, instead of droppi…
wishborn Sep 14, 2026
ea3c4fa
Send a text document to Anthropic as text/plain, whatever text type i…
wishborn Sep 14, 2026
04e233c
Read thinking: false as no thinking, not a shape to send
wishborn Sep 14, 2026
b787ded
Send the default thinking budget as an integer
wishborn Sep 14, 2026
f062dd6
Keep no token in the release guard's checkout
wishborn Sep 14, 2026
565ce16
Make the documented tool-approval resume work through a real handler
wishborn Sep 14, 2026
034fe8f
Never run a tool call again once it has a result, approved or not
wishborn Sep 14, 2026
5a39367
Publish from publish.yml, the workflow name every package uses
wishborn Sep 14, 2026
7333254
Keep credentials out of a failed fetch's message (#45)
wishborn Sep 17, 2026
e95012d
Order Media's new helper as Rector wants it
wishborn Sep 17, 2026
0e2ce28
Make the allowlist gate block the install instead of racing it
wishborn Sep 21, 2026
fe46390
Gate the installs performed by an ACTION, which no audit could see
wishborn Sep 21, 2026
9cb9330
Stop naming three packages as unpublished when all three are published
wishborn Sep 22, 2026
db4be94
Carry the tools a model was offered past the content gate
wishborn Sep 22, 2026
380e28c
Order the new methods as Rector wants them
wishborn Sep 22, 2026
70a0393
Digest an empty parameter map as an object, so a port can reproduce it
wishborn Sep 22, 2026
85b5253
A streamed turn reports every step's output, and each step its own usage
wishborn Sep 22, 2026
ec6fee3
Give every Anthropic message its own usage baseline
wishborn Sep 22, 2026
e855074
A null content or results list is empty, not a failed generation (fix…
wishborn Sep 22, 2026
72ad953
Mark a message for one request without marking it forever (fixes #59)
wishborn Sep 22, 2026
f3f2b8f
Read Z.ai stream usage from whichever chunk carries it (fixes #60)
wishborn Sep 22, 2026
fb188ea
Z stream: record a response's usage once, not once per chunk that car…
wishborn Sep 22, 2026
fb931d0
docs: clarify usage guidance, telemetry tool metadata and Z endpoint
wishborn Sep 22, 2026
de7b2a5
A cache mark on a message is one breakpoint, on its final block
wishborn Sep 22, 2026
5fa9385
Ollama and Vertex streamed steps report what the step cost
wishborn Sep 22, 2026
e211510
An opt-in guarded fetch, for a URL you did not choose
wishborn Sep 22, 2026
93bca6a
Declare ext-filter, which the guarded fetch uses
wishborn Sep 22, 2026
c8d10a6
Declare which parts of a request are stable, and let Anthropic place …
wishborn Sep 22, 2026
ee5daad
Support laravel/mcp 1.0 in LaravelMcpTool
wishborn Sep 22, 2026
18c93ba
Say so when the message order defeats prompt caching
wishborn Sep 22, 2026
8a8dfed
Gemini caches the declared-stable prefix, instead of the caller doing…
wishborn Sep 22, 2026
cb96412
Gemini's streamed and structured generations resolve the stable prefi…
wishborn Sep 22, 2026
615a5be
Vertex can reference a cached resource, and derives one from the hint
wishborn Sep 22, 2026
07f842b
Every guarded-fetch refusal carries a code, not just a sentence
wishborn Sep 22, 2026
f3e4238
Drive the media-fetch-guard corpus, rather than claiming to
wishborn Sep 22, 2026
d823c39
Drive the cache-stability-hints corpus against the reference
wishborn Sep 22, 2026
6f84ea0
fix(perplexity): retain typed unsuccessful run diagnostics
wishborn Sep 24, 2026
fffac8a
fix(perplexity): reject unsuccessful stream terminals with diagnostics
wishborn Sep 24, 2026
33cc429
fix(perplexity): preserve stream endings and rate-limit classification
wishborn Sep 24, 2026
b4d67da
docs(perplexity): explain unknown stream status tradeoff
wishborn Sep 24, 2026
eb18824
fix(openai): surface chat completion refusals
wishborn Sep 30, 2026
a24a73d
fix(embeddings): decode base64 float32 provider responses
wishborn Sep 30, 2026
84f7ca4
fix(ci): enforce strict Prism factcheck before release
wishborn Sep 30, 2026
f96c7be
test(docs): verify provider support through request dispatch
wishborn Sep 30, 2026
1002a1a
docs: verify rate limits and finish claim corrections
wishborn Sep 30, 2026
b6f050c
Patch the two dev-scope advisories in the docs toolchain
wishborn Oct 1, 2026
f08b229
G-71: the redirect hop bound was held off by a line nothing exercised
wishborn Oct 1, 2026
015a57c
test(media): compare the redirect hop-bound refusal
wishborn Oct 1, 2026
c6a2a2d
Raise the Laravel floor off a vulnerable range, and audit PHP depende…
wishborn Oct 2, 2026
f7e585e
Docs: lead image generation with gpt-image-1, and check the APIs we d…
wishborn Oct 2, 2026
caa27b1
Apply Rector to the new documentation test
wishborn Oct 2, 2026
537cc81
G-78: analyse the doc snippets, and it finds real defects
wishborn Oct 5, 2026
35dd3cb
Vacuity guards on the fence extractor, and they caught themselves first
wishborn Oct 5, 2026
bc5513c
Fix the documentation defects the fence analyser found: 60 findings d…
wishborn Oct 5, 2026
d35c97f
Refuse a release whose notes do not say whether it breaks anything
wishborn Oct 5, 2026
003e229
Guard the vendored release-notes checker against the canonical one
wishborn Oct 5, 2026
5ab9346
Bring the cache-stability fixture back in step with the corpus
wishborn Oct 7, 2026
2029f56
fix(openai): give gpt-6.x and o-series models strict structured output
wishborn Oct 15, 2026
4fd3725
fix(gemini): report a CONTINUATION finish as Length
wishborn Oct 15, 2026
3446eca
docs(gemini,vertex): replace delisted models in examples
wishborn Oct 15, 2026
11a1e3e
fix(media): refuse CGNAT, NAT64/6to4 spellings and other non-public r…
wishborn Oct 15, 2026
ba1862a
test(media): pin the dotted NAT64 spelling of the metadata endpoint a…
wishborn Oct 15, 2026
5918692
test(media): vendor the G-89 media-fetch-guard rows, and a third control
wishborn Oct 18, 2026
cec4c55
test(media): the vendored G-89 rows themselves (the previous commit s…
wishborn Oct 18, 2026
3c3cf04
fix(media): cap the guarded fetch's body, refused while it streams (G…
wishborn Oct 19, 2026
ed5bcc4
chore(docs): update source-map-js to 1.2.2 in the docs lock
wishborn Oct 19, 2026
5f9dadd
chore(docs): update vue to 3.5.44 in the docs lock
wishborn Oct 19, 2026
7664568
Merge upstream prism-php/prism main (b620af4, #1043) into the fork
wishborn Oct 19, 2026
e102970
ci: allow the gate workflows to be dispatched by hand
wishborn Oct 20, 2026
93f5302
feat(tool): decode an object or array argument a model sent as a stri…
wishborn Oct 21, 2026
cab335f
style(tests): cast the tool result before decoding it, as rector asks
wishborn Oct 21, 2026
72c8f1b
fix(streaming): restore StreamCollector, removed in v0.132.0 without …
wishborn Oct 21, 2026
0f84cf0
style(tests): import Message, as pint and rector settle on
wishborn Oct 21, 2026
2253c27
docs: name the guarded fetch for untrusted URLs, and fix the examples…
wishborn Oct 21, 2026
85e8d14
fix(anthropic): report a stream that ends before message_stop as an e…
wishborn Oct 24, 2026
633ac76
chore(deps): drop the redundant direct pest-plugin-arch requirement
wishborn Oct 24, 2026
527ead3
chore(docs): update vulnerable build tooling (#86)
wishborn Oct 24, 2026
0ff1197
fix(openrouter): account for complete BYOK billing (#87)
wishborn Oct 24, 2026
deddba4
fix(azure): never run an approval-required tool while streaming (#88)
wishborn Oct 25, 2026
5411d65
docs: correct invalid examples and capability claims
wishborn Oct 26, 2026
b486d66
docs: complete provider guides and Azure reference
wishborn Oct 26, 2026
c99b6b8
docs(core): complete request, streaming and media guides
wishborn Oct 26, 2026
642501a
docs(guides): complete setup, advanced and media usage
wishborn Oct 26, 2026
3dcf4fc
docs(companions): complete PHP setup and usage guides
wishborn Oct 26, 2026
f4d8210
docs(audit): finish support matrix and example validation
wishborn Oct 26, 2026
d66f38d
fix(replicate): isolate output downloads from API credentials (#95)
wishborn Oct 26, 2026
4aef812
fix(tool): honor shouldRegister on Laravel MCP tools
nbullorini Oct 26, 2026
3c3881a
Merge pull request #96 from Particle-Academy/absorb/upstream-1048-mcp…
wishborn Oct 27, 2026
548c41d
ci: pin and verify shared GitHub checks (#97)
wishborn Oct 27, 2026
3e05c75
ci: harden shared-check verifier and execution ordering (#98)
wishborn Oct 27, 2026
8a73ccd
feat(telemetry): report tool failures without content capture (#100)
wishborn Oct 29, 2026
19b2d77
fix(tools)!: require readonly-compatible closure serialization (#101)
wishborn Oct 29, 2026
06aa083
fix: preserve measured embedding corpus observations (#102)
wishborn Oct 30, 2026
b2dab97
fix(perplexity): repair agent requests and deep research diagnostics …
wishborn Oct 30, 2026
bebcabd
feat(tools): add application error policy (#106)
wishborn Oct 31, 2026
0965f5e
feat(providers): expose pure request payload projection (#107)
wishborn Oct 31, 2026
c131f2d
feat: expose optional projected payload wire formats (#108)
wishborn Oct 31, 2026
82f4ad0
fix(mistral): extract text from structured content blocks (#110)
wishborn Nov 1, 2026
4e168a9
fix(groq): key streamed tool calls by the delta's index, not its posi…
pcbeingused333 Oct 29, 2026
9d4ce44
test(groq): cover sparse and legacy tool-call deltas
wishborn Oct 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
19 changes: 18 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Normalise text to LF in the repository and on checkout, so a Windows clone
# does not report every file as modified after a formatter run.
* text=auto eol=lf

/.github export-ignore
/assets export-ignore
/bin export-ignore
Expand All @@ -9,4 +13,17 @@
/rector.php export-ignore
/Taskfile.yml export-ignore
/testbench.yaml export-ignore
/whisky.json export-ignore
/dev export-ignore
/composer-require-checker.json export-ignore

# /docs itself SHIPS — the docs site renders markdown and the VitePress
# sidebar out of vendor/particle-academy/prism/docs, and installs with
# --prefer-dist, so export-ignoring the directory would take that site down.
# Only the parts nothing reads at runtime are excluded: the 9.6M committed
# build output, and the dev lockfile that makes dependency scanners flag
# consumers for advisories in our docs toolchain.
/docs/.vitepress/dist export-ignore
/docs/.vitepress/cache export-ignore
/docs/node_modules export-ignore
/docs/package.json export-ignore
/docs/package-lock.json export-ignore
12 changes: 12 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Every path in this repository is owned by the project owner.
#
# Paired with the "main protection" branch ruleset, which requires a pull
# request carrying an approving review from a code owner before anything can
# land on the default branch. Outside contributions therefore cannot merge
# without an explicit human review, and agents cannot approve on the owner's
# behalf.
#
# Repository admins bypass the ruleset, so day-to-day direct pushes by the
# owner are unaffected.

* @wishborn
50 changes: 50 additions & 0 deletions .github/RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Prism release preflight

Complete this checklist on the release candidate **before pushing a tag**.
Packagist publishes from the tag push. The Publish workflow can withhold the
GitHub release and flag a bad tag; it cannot prevent or undo that publication.

- [ ] Fetch current Prism and prism-parity main branches and Prism tags. Record
the full candidate SHA. Do not overwrite another contributor's working files.
- [ ] Re-read every Prism claim against the candidate code, including unreleased
fixes. Use the shared checker's `--map` output as an inventory, and check nearby
behavior and numeric statements too: existence checks do not verify prose.
Fix drift and record what changed before reconciling. Commit the reviewed
Prism changes so the candidate checkout is clean.
- [ ] If the Prism entry is stale, run the manual command below with the full
SHA that was reviewed. Inspect and commit only the Prism changes in the shared
lock, describe the reviewed SHA in that commit, and push prism-parity.
- [ ] Run all four local gates: Pest, PHPStan, Pint plus Rector, and
composer-require-checker. Run the factcheck guard tests and the strict checker.
- [ ] Confirm successful Tests, PHPStan, Formatting, Require Checker and
**strict Factcheck** CI runs on the exact candidate SHA. A successful run on a
previous commit or a PR merge commit is insufficient. After reconciling the
shared lock, rerun Factcheck on the candidate using workflow_dispatch.
- [ ] Complete the prerelease security audit and obtain release approval.
Only the approved candidate may be tagged. Any candidate change repeats the
review and CI steps.

From the Prism repository root, with prism-parity checked out beside it:

```sh
node ../prism-parity/tools/factcheck.mjs --repo . --map
node .github/scripts/factcheck.mjs reconcile ../prism-parity FULL_REVIEWED_SHA
node --test .github/scripts/factcheck.test.mjs .github/scripts/release-factcheck.test.mjs
node .github/scripts/factcheck.mjs check ../prism-parity
```

The manual helper requires the supplied SHA to equal HEAD and a clean Prism
checkout. The shared checker's raw `--reconcile` replaces the whole lock even
with `--repo`; do not use it directly for a Prism-only review. The helper runs
that operation in a temporary copy and merges only the Prism repo and package
entries back into `prism-parity/tools/factcheck.lock.json`. Sibling evidence and
the global reconciliation timestamp remain unchanged. Review the diff and stage
that file explicitly, never unrelated work in prism-parity. CI never reconciles.

The lock uses the nearest reachable tag, not a content digest. A new tag makes
the lock stale even when the code is identical: the first push after each
release will fail strict until its claims are reviewed and reconciled. The
failure prints the lock path, recorded and current versions, and the manual
command. Changes on the same tag are not detected by this currency heuristic;
the pre-tag review remains necessary. A shared content-based attestation is
separate work, not implemented by these guards.
86 changes: 86 additions & 0 deletions .github/scripts/factcheck.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { basename, join, resolve } from 'node:path';
import { execFileSync, spawnSync } from 'node:child_process';

const [mode, parityArg, reviewedSha] = process.argv.slice(2);
const repo = process.cwd();
const parity = resolve(parityArg ?? '.parity');
const lockPath = join(parity, 'tools/factcheck.lock.json');
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
const read = path => JSON.parse(readFileSync(path, 'utf8'));
const run = (root, ...args) => {
const result = spawnSync(process.execPath, [join(root, 'tools/factcheck.mjs'), '--repo', repo, ...args], { stdio: 'inherit' });
if (result.error) throw result.error;
return result.status ?? 1;
};

/**
* Manual only: the shared --reconcile is not scope-safe, even with --repo.
* Run it in a temporary copy, then merge only repos.prism and
* packages['particle-academy/prism']; never replace any sibling's evidence.
* A reviewed SHA is an explicit operator assertion, not proof of a review.
*/
function reconcile() {
if (!reviewedSha || !/^[a-f0-9]{40}$/.test(reviewedSha) || reviewedSha !== git('rev-parse', 'HEAD')) {
throw new Error('Supply the full reviewed SHA, equal to HEAD, after re-reading every claim.');
}
if (git('status', '--porcelain', '--untracked-files=normal')) {
throw new Error('Reconciliation requires a clean Prism checkout; commit the reviewed changes first.');
}
const before = readFileSync(lockPath, 'utf8');
const original = JSON.parse(before);
if (original.contract !== '1.0' || !original.repos?.prism || !original.packages?.['particle-academy/prism']) {
throw new Error('Expected the existing contract 1.0 Prism entries; refusing to replace an unknown lock.');
}
const temp = mkdtempSync(join(tmpdir(), 'prism-reconcile-'));
try {
mkdirSync(join(temp, 'tools'));
cpSync(join(parity, 'tools/factcheck.mjs'), join(temp, 'tools/factcheck.mjs'));
cpSync(join(parity, 'docs/decisions'), join(temp, 'docs/decisions'), { recursive: true });
writeFileSync(join(temp, 'tools/factcheck.lock.json'), before);
if (run(temp, '--reconcile') !== 0) throw new Error('Reconciliation refused; the shared lock is unchanged.');
const updated = read(join(temp, 'tools/factcheck.lock.json'));
if (updated.packages?.['particle-academy/prism']?.published !== true) {
throw new Error('Could not confirm Prism publication; the shared lock is unchanged. Retry with network access.');
}
original.repos.prism = updated.repos.prism;
original.packages['particle-academy/prism'] = updated.packages['particle-academy/prism'];
// Validate the actual merged result, including preserved sibling publication
// evidence used by Prism's companion-package install documentation.
writeFileSync(join(temp, 'tools/factcheck.lock.json'), JSON.stringify(original, null, 2) + '\n');
if (run(temp, '--strict') !== 0) throw new Error('Reconciled claims still fail; the shared lock is unchanged.');
if (readFileSync(lockPath, 'utf8') !== before || git('rev-parse', 'HEAD') !== reviewedSha || git('status', '--porcelain', '--untracked-files=normal')) {
throw new Error('The lock or reviewed checkout changed during reconciliation. Re-read and retry.');
}
writeFileSync(lockPath, JSON.stringify(original, null, 2) + '\n');
console.log(`Reconciled only Prism at reviewed SHA ${reviewedSha} into ${lockPath}.`);
} finally {
rmSync(temp, { recursive: true, force: true });
}
}

try {
if (basename(repo) !== 'prism' || read(join(repo, 'composer.json')).name !== 'particle-academy/prism') {
throw new Error('Run this command from the prism repository root.');
}
if (mode === 'reconcile') {
reconcile();
} else if (mode === 'check') {
const status = run(parity, '--strict');
if (status !== 0) {
console.error(`\nVerification lock: ${lockPath}`);
console.error(`Recorded Prism version: ${read(lockPath).repos?.prism?.version ?? 'unrecorded'}`);
console.error(`Current Prism version: ${git('describe', '--tags', '--abbrev=0')}`);
console.error('Fix findings and re-read the claims against current code BEFORE reconciling. From a clean Prism checkout with prism-parity beside it, run:');
console.error(`node .github/scripts/factcheck.mjs reconcile ../prism-parity ${git('rev-parse', 'HEAD')}`);
console.error('Review and commit only the Prism lock entry in prism-parity, push it, then rerun Factcheck. See .github/RELEASING.md. CI never reconciles.');
}
process.exitCode = status;
} else {
throw new Error('Usage: node .github/scripts/factcheck.mjs check <parity-path> | reconcile <parity-path> <reviewed-sha>');
}
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
139 changes: 139 additions & 0 deletions .github/scripts/factcheck.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { execFileSync, spawnSync } from 'node:child_process';

const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const source = existsSync(join(root, '.parity/tools/factcheck.mjs'))
? join(root, '.parity') : resolve(root, '../prism-parity');

function fixture(t) {
const dir = mkdtempSync(join(tmpdir(), 'prism-factcheck-'));
t.after(() => rmSync(dir, { recursive: true, force: true }));
const repo = join(dir, 'prism');
const parity = join(dir, 'prism-parity');
mkdirSync(join(repo, 'src'), { recursive: true });
mkdirSync(join(parity, 'tools'), { recursive: true });
cpSync(join(source, 'tools/factcheck.mjs'), join(parity, 'tools/factcheck.mjs'));
cpSync(join(source, 'docs/decisions'), join(parity, 'docs/decisions'), { recursive: true });
writeFileSync(join(repo, 'composer.json'), JSON.stringify({
name: 'particle-academy/prism', autoload: { 'psr-4': { 'Prism\\Prism\\': 'src/' } },
}));
writeFileSync(join(repo, 'src/Example.php'), '<?php namespace Prism\\Prism; class Example {}');
writeFileSync(join(repo, 'README.md'), '```php\nuse Prism\\Prism\\Example;\n```\n');
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
git('init', '-q');
git('add', '.');
git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '-qm', 'fixture');
git('tag', 'v0.130.0');
const sha = git('rev-parse', 'HEAD');
const lock = join(parity, 'tools/factcheck.lock.json');
const initial = {
contract: '1.0', reconciledAt: 'original timestamp', note: 'preserve this',
packages: { 'particle-academy/prism': { published: true }, 'particle-academy/sibling': { published: false, sentinel: 1 } },
repos: { prism: { version: 'v0.121.0', census: { 'php-class': 1 } }, sibling: { version: 'v9.9.9', census: { 'local-link': 42 } } },
};
writeFileSync(lock, JSON.stringify(initial, null, 2) + '\n');
// Only the test process tree stubs publication lookup; no network is needed.
const preload = join(dir, 'fetch.mjs');
writeFileSync(preload, 'globalThis.fetch = async () => ({status: 200});');
const run = (mode, reviewed = sha) => spawnSync(process.execPath,
[join(root, '.github/scripts/factcheck.mjs'), mode, parity, ...(mode === 'reconcile' ? [reviewed] : [])],
{ cwd: repo, encoding: 'utf8', env: { ...process.env, NODE_OPTIONS: `--import=${pathToFileURL(preload).href}` } });
return { repo, parity, sha, lock, initial, run, git, preload };
}

test('stale claims fail with versions, lock path and a manual command; checking never writes', t => {
const f = fixture(t);
const before = readFileSync(f.lock, 'utf8');
const result = f.run('check');
assert.equal(result.status, 1);
const output = result.stdout + result.stderr;
for (const value of ['v0.121.0', 'v0.130.0', 'factcheck.lock.json', 'reconcile', f.sha]) assert.ok(output.includes(value), output);
assert.equal(readFileSync(f.lock, 'utf8'), before);
});

test('reviewed reconciliation preserves every sibling entry and unrelated lock field', t => {
const f = fixture(t);
const result = f.run('reconcile');
assert.equal(result.status, 0, result.stdout + result.stderr);
const after = JSON.parse(readFileSync(f.lock, 'utf8'));
assert.equal(after.repos.prism.version, 'v0.130.0');
assert.deepEqual(after.repos.prism.census, { 'php-class': 1 });
assert.equal(after.packages['particle-academy/prism'].published, true);
after.repos.prism = f.initial.repos.prism;
after.packages['particle-academy/prism'] = f.initial.packages['particle-academy/prism'];
assert.deepEqual(after, f.initial);
const checked = f.run('check');
assert.equal(checked.status, 0, checked.stdout + checked.stderr);
});

test('a wrong or absent reviewed SHA cannot change the lock', t => {
const f = fixture(t);
const before = readFileSync(f.lock, 'utf8');
for (const reviewed of ['', '0'.repeat(40)]) {
const result = f.run('reconcile', reviewed);
assert.notEqual(result.status, 0);
assert.match(result.stderr, /reviewed.*SHA/i);
assert.equal(readFileSync(f.lock, 'utf8'), before);
}
});

test('uncommitted claim edits cannot be attested by a committed SHA', t => {
const f = fixture(t);
const before = readFileSync(f.lock, 'utf8');
writeFileSync(join(f.repo, 'README.md'), 'Edited after review');
const result = f.run('reconcile');
assert.notEqual(result.status, 0);
assert.match(result.stderr, /clean|uncommitted/i);
assert.equal(readFileSync(f.lock, 'utf8'), before);
});

test('a broken documented class refuses reconciliation and preserves the entire lock', t => {
const f = fixture(t);
writeFileSync(join(f.repo, 'README.md'), '```php\nuse Prism\\Prism\\Missing;\n```\n');
f.git('add', 'README.md');
f.git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '-qm', 'broken claim');
const before = readFileSync(f.lock, 'utf8');
const result = f.run('reconcile', f.git('rev-parse', 'HEAD'));
assert.notEqual(result.status, 0);
assert.match(result.stdout + result.stderr, /unresolved finding|No such class/);
assert.equal(readFileSync(f.lock, 'utf8'), before);
});

test('an unavailable publication lookup cannot overwrite verified publication data', t => {
const f = fixture(t);
writeFileSync(f.preload, 'globalThis.fetch = async () => { throw new Error("offline"); };');
const before = readFileSync(f.lock, 'utf8');
const result = f.run('reconcile');
assert.notEqual(result.status, 0);
assert.match(result.stderr, /publication/i);
assert.equal(readFileSync(f.lock, 'utf8'), before);
});

test('a concurrent shared-lock edit is preserved instead of overwritten', t => {
const f = fixture(t);
const concurrent = JSON.stringify({ ...f.initial, note: 'another review landed' }, null, 2) + '\n';
writeFileSync(f.preload, `import {writeFileSync} from 'node:fs'; globalThis.fetch = async () => {
writeFileSync(${JSON.stringify(f.lock)}, ${JSON.stringify(concurrent)}); return {status: 200};
};`);
const result = f.run('reconcile');
assert.notEqual(result.status, 0);
assert.match(result.stderr, /changed during reconciliation/);
assert.equal(readFileSync(f.lock, 'utf8'), concurrent);
});

test('post-reconcile verification uses the preserved sibling publication evidence', t => {
const f = fixture(t);
f.initial.packages['particle-academy/sibling'].published = true;
writeFileSync(f.lock, JSON.stringify(f.initial));
writeFileSync(join(f.repo, 'README.md'), '```sh\ncomposer require particle-academy/sibling\n```\n');
f.git('add', 'README.md');
f.git('-c', 'user.name=Test', '-c', 'user.email=test@example.invalid', 'commit', '-qm', 'sibling install');
const result = f.run('reconcile', f.git('rev-parse', 'HEAD'));
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.doesNotMatch(result.stdout + result.stderr, /Not in the published census/);
});
24 changes: 24 additions & 0 deletions .github/scripts/release-factcheck.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { execFileSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';

export function requireFactcheck(runs, sha) {
// PR workflows can execute a synthetic merge commit instead of head_sha.
const found = runs.some(run => run.head_sha === sha && run.status === 'completed'
&& run.conclusion === 'success' && ['push', 'workflow_dispatch', 'schedule'].includes(run.event));
if (!found) throw new Error(`No successful strict Factcheck run for ${sha}. Refusing to release.`);
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
const { GITHUB_REPOSITORY: repo, GITHUB_SHA: sha } = process.env;
if (!repo || !/^[a-f0-9]{40}$/.test(sha ?? '')) throw new Error('GITHUB_REPOSITORY and full GITHUB_SHA are required.');
const pages = JSON.parse(execFileSync('gh', ['api', '--paginate', '--slurp',
`repos/${repo}/actions/workflows/factcheck.yml/runs?head_sha=${sha}&per_page=100`,
], { encoding: 'utf8' }));
requireFactcheck(pages.flatMap(page => page.workflow_runs), sha);
console.log(`Strict Factcheck succeeded for ${sha}.`);
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
19 changes: 19 additions & 0 deletions .github/scripts/release-factcheck.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { requireFactcheck } from './release-factcheck.mjs';

const sha = 'a'.repeat(40);
const success = { head_sha: sha, status: 'completed', conclusion: 'success', event: 'push' };

test('only completed successful checks on the exact commit qualify', () => {
assert.doesNotThrow(() => requireFactcheck([success], sha));
for (const runs of [[], [{ ...success, head_sha: 'b'.repeat(40) }], [{ ...success, status: 'in_progress' }], [{ ...success, conclusion: 'failure' }], [{ ...success, conclusion: 'skipped' }], [{ ...success, event: 'pull_request' }]]) {
assert.throws(() => requireFactcheck(runs, sha), /No successful strict Factcheck/);
}
});

test('manual and nightly runs can prove the exact candidate, regardless of unrelated runs', () => {
for (const event of ['workflow_dispatch', 'schedule']) {
assert.doesNotThrow(() => requireFactcheck([{ ...success, head_sha: 'b'.repeat(40) }, { ...success, event }], sha));
}
});
Loading
Loading