Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 35 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,15 @@ databases, credentials and migrations. The product uses Core exclusively; it has
discovered during a task without switching work or automatically selecting them
next. Only a direct acceptance blocker justifies a minimal in-scope fix. After
each bounded task passes checks/review and merges, mark its child entry done.
Select large Core tasks in order from the concise TODO, then use the full board
to choose bounded children by value, dependencies, risk and effort. Finish the
selected large task before switching to the next one. The main agent selects
tasks; subagents are for technical design, scoped collaboration and review,
not prioritization. Completing a child or milestone does not stop an explicitly
active long-term goal. Product integration, UI and business Team work remain
Treat each selected concise Core TODO item as a delivery milestone. The long-term
Goal retains the objective, constraints and standards; detailed-board entries are
smaller tasks within that milestone. Batch related small tasks when they share a
functional outcome and safety boundary, then validate and independently review
the batch once stable. Do not manufacture one PR per internal wiring step.
Finish the selected milestone before choosing another. The main agent selects
tasks; subagents handle technical design, scoped collaboration and review, not
prioritization. Stop after the complete milestone when the user sets that boundary;
do not automatically claim the next one or mark the long-term objective complete. Product integration, UI and business Team work remain
outside Core delivery. Prioritize a sound architecture skeleton
and correct principal workflows with real API validation. Record and defer
low-frequency corner cases when risk and ROI permit; do not let minor details
Expand Down Expand Up @@ -1937,6 +1940,32 @@ foreign, ambiguous or metadata-only history rejects before model input. The Runt
volume and shared Environment/Session binding establish ownership; this lookup
cannot select another Session's home or infer ownership from a model response.

### Deferred function discovery

Public `tool_search` and function `defer_loading` are shared Runtime intent.
Core preserves complete immutable definitions, sends `PromptRequestPayload.ToolSearch`
and each `FunctionTool.DeferLoading`, and requires the operation's existing profile
qualification plus the Runtime `tool_search` capability. It never performs native
search, selects native names, interprets provider policies or implements another
model/tool loop. Additional harnesses implement the same intent in their adapters.
The pinned Session AgentTool response union excludes the tool_search input member;
project it out of Session/SSE resources while preserving saved and frozen input.

The bounded implementation targets Claude's single-agent `environment:none`
function profile, including qualified inline message images. The adapter explicitly enables native ToolSearch and sets
per-function MCP `anthropic/alwaysLoad` from the requested deferral flag. Ordinary
functions stay eager. Existing function callbacks, application receipts, cancellation
and cold continuation remain the only execution/result lifecycle. Runtime discovery
advertises this operation only with an installed bridge supporting `tool_search`.

Known conflicting native provider modes and search/beta settings reject in the
adapter. The maintained native harness owns dynamic model/provider eligibility;
its SDK exposes no reliable pre-input receipt proving effective deferral after a
policy change. Do not represent tool inventory or an operator allowlist as that
proof. Record exact real model/provider evidence and this detection gap separately.
Search-only, missing-search, duplicate-search, workspace, MCP and Subagent combinations
remain unqualified. See [the operation coverage](contracts/agents-api/tool-search.md).

### Structured output execution

The public `text.format={type:"json_schema",schema:{...}}` is resolved with saved
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,26 @@ func TestStructuredOutputConfigurationReachesNativeUnchanged(t *testing.T) {
t.Fatal("unqualified subagent combination accepted")
}
}

func TestToolDiscoveryPreservesFrozenFunctionsAndRejectsOtherProfiles(t *testing.T) {
root := t.TempDir()
t.Setenv("PARSAR_HOME", root)
config := Config{Entrypoint: filepath.Join(root, "worker"), StateDir: filepath.Join(root, "state")}
request := proto.PromptRequestPayload{RunID: "run", Input: proto.TextInput("Original input."), DisableSubagents: true, ToolSearch: true, AgentOptions: map[string]any{"model": "model"}, FunctionTools: []proto.FunctionTool{
{Name: "lookup", Description: "Lookup", Parameters: json.RawMessage(`{"type":"object","properties":{"ticket":{"const":"original"}}}`), DeferLoading: true},
{Name: "clock", Description: "Clock", Parameters: json.RawMessage(`{"type":"object"}`)},
}}
start, _, err := prepare(config, request)
if err != nil || !start.ToolSearch || !reflect.DeepEqual(start.Functions, request.FunctionTools) {
t.Fatal("function discovery changed native definitions", err)
}
request.DisableSubagents = false
if _, _, err := prepare(config, request); err == nil {
t.Fatal("unqualified combination admitted")
}
request.DisableSubagents = true
request.ToolSearch = false
if _, _, err := prepare(config, request); err == nil {
t.Fatal("deferred definitions became eager")
}
}
10 changes: 10 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type subagentOptions struct {
}

type startRequest struct {
ToolSearch bool `json:"tool_search,omitempty"`
Subagents *subagentOptions `json:"subagents,omitempty"`
OutputFormat *proto.OutputFormat `json:"output_format,omitempty"`
Type string `json:"type"`
Expand Down Expand Up @@ -59,6 +60,15 @@ func prepareConfiguration(config Config, req proto.PromptRequestPayload) (startR
if req.WorkspaceAuthoring || req.ObserveTools {
return fail("requested capability is not available in the private SDK adapter")
}
if err := req.ValidateToolSearch(true); err != nil {
return startRequest{}, nil, err
}
if req.ToolSearch {
if config.Workspace != nil || req.LocalEnvironment != nil || req.MCPHTTPServers != nil || !req.DisableSubagents || (req.ExecutionControls != nil && req.ExecutionControls.OutputFormat != nil) {
return fail("tool discovery requires the single-agent text/function profile")
}
start.ToolSearch = true
}
if err := validateMCP(req); err != nil {
return startRequest{}, nil, err
}
Expand Down
4 changes: 4 additions & 0 deletions apps/parsar-daemon/internal/agent/claudesdk/readiness.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ func (info RuntimeInfo) SupportsMessageImages() bool {
return slices.Contains(info.Features, "message_images")
}

func (info RuntimeInfo) SupportsToolSearch() bool {
return slices.Contains(info.Features, "tool_search")
}

func (info RuntimeInfo) SupportsStructuredOutput() bool {
return slices.Contains(info.Features, "structured_output")
}
Expand Down
1 change: 1 addition & 0 deletions apps/parsar-daemon/internal/cli/claude_sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ func discoverClaudeSDK(rc *runContext, profile string, check func(context.Contex
}
out.Info.Available, out.Info.Version = true, info.SDK
out.Info.Capabilities.MessageImages = info.SupportsMessageImages()
out.Info.Capabilities.ToolSearch = out.Config.Workspace == nil && info.SupportsToolSearch()
out.Info.Capabilities.StructuredOutput = out.Config.Workspace == nil && info.SupportsStructuredOutput()
out.Info.Capabilities.SubagentObservations = info.SupportsSubagents()
out.Info.Capabilities.MCPHTTPTools = info.SupportsHTTPMCP()
Expand Down
3 changes: 3 additions & 0 deletions apps/parsar-daemon/internal/dispatch/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import (
)

func validateExecutionEnvironment(req proto.PromptRequestPayload, caps proto.AgentKindCapabilities) error {
if err := req.ValidateToolSearch(caps.ToolSearch); err != nil {
return err
}
if req.LocalEnvironment != nil && (req.DisableExecutionEnvironment || !caps.LocalEnvironment) {
return errors.New("engine does not support this local Environment configuration")
}
Expand Down
17 changes: 17 additions & 0 deletions apps/parsar-daemon/internal/dispatch/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,20 @@ func functionResultContent(text string) []proto.InputContent {
after := "AFTER-IMAGE"
return []proto.InputContent{{Type: "input_text", Text: &text}, {Type: "input_image", ImageURL: &imageURL}, {Type: "input_text", Text: &after}}
}

func TestDiscoveryCannotReachAnEagerOnlyAdapter(t *testing.T) {
reg := agent.NewRegistry()
called := false
reg.RegisterKind(proto.SupportedAgentKind{Kind: "eager-only", Available: true, Capabilities: proto.AgentKindCapabilities{FunctionTools: true}}, func(context.Context, proto.PromptRequestPayload, chan<- proto.Envelope) (agent.Session, error) {
called = true
return nil, nil
})
router, _ := dispatch.New(dispatch.Config{Registry: reg, Sender: &recSender{}})
defer router.Shutdown(context.Background())
for _, search := range []bool{false, true} {
env, _ := proto.NewEnvelope(proto.TypePromptRequest, "discovery", proto.PromptRequestPayload{AgentKind: "eager-only", ToolSearch: search, FunctionTools: []proto.FunctionTool{{Name: "lookup", Parameters: json.RawMessage(`{"type":"object"}`), DeferLoading: true}}})
if err := router.Handle(t.Context(), env); err == nil || called {
t.Fatal("deferred definitions reached an eager-only adapter", err)
}
}
}
7 changes: 4 additions & 3 deletions contracts/agents-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ user-managed enrollment remain outside this qualification.
| Subagents / multi_agent | Six reads and same-child recovery have three-harness Docker evidence; optional native operations, live child progress, full lifecycle/interactions and tool combinations remain explicit gaps |
| Environment Templates | Unsupported restricted hostname forms, unqualified installation overrides/null network and exact hosted errors remain gaps. CRUD/list, files, env/setup/system/npm/Python, inline/referenced Skills, Plugins, workspace capability directories and Session references have recorded coverage. Environment Plugin MCP transport and placement limits are [listed separately](environment-templates.md#environment-origin-mcp-plugins) |
| Input and configuration | Non-text initial input, broader content/configuration unions and reasoning/verbosity combinations; [structured output](structured-output.md) has a qualified Claude function profile, with other combinations remaining gaps |
| Tools and interactions | Deferred functions, other tool types, effective tool-set enforcement and result/cancel publication ordering; MiniMax public functions and service-origin MCP remain unsupported |
| Tools and interactions | [Deferred discovery qualification](tool-search.md), other tool types, effective tool-set enforcement and result/cancel publication ordering; MiniMax public functions and service-origin MCP remain unsupported |
| Vault and Credentials | OAuth/refresh, archive semantics, revocation/concurrent mutation and exact hosted selection/error behavior; static bearer CRUD/token replacement is already present |
| Existing resources | Full Item/SSE/Usage variants, omitted/null/default/error semantics, pagination and overlapping lifecycle behavior beyond recorded cases |

Expand Down Expand Up @@ -631,12 +631,13 @@ server validation define the supported alternatives.

### Public function configuration

Inline `agent.tools` accepts non-deferred `function` definitions with the upstream
Inline `agent.tools` accepts `function` definitions with the upstream
required name, description and JSON Schema parameter object. Missing
`defer_loading` resolves to `false`; null and other types are rejected. Omitted,
null and empty tool lists resolve to an empty list. The resolved tools are part of
the immutable Session configuration and creation retry identity. Saved-Agent
inheritance uses the same resolved tools. Deferred discovery, other tool kinds,
inheritance uses the same resolved tools. The bounded [deferred discovery path](tool-search.md)
adds type-only `tool_search` for its qualified profile. Other discovery combinations, other tool kinds,
the native 64-definition cap and unique nonblank names of at most 512 bytes remain
compatibility gaps. Claude SDK additionally requires object-root schemas and
text-only results. Codex internal Goal/Skills/user-input/discovery semantics need
Expand Down
2 changes: 1 addition & 1 deletion contracts/agents-api/harness-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ An engine without native tools can guarantee their absence; an engine with tools
must actually disable them when requested. Configuration acceptance is not proof
of enforcement.

MCP, public function calls, structured output, image inputs, verbosity controls and other optional
MCP, public function calls, deferred function discovery, structured output, image inputs, verbosity controls and other optional
operations do not need to match another engine. Reject unqualified combinations
explicitly and record the gap. Never advertise a capability to bypass selection.

Expand Down
1 change: 1 addition & 0 deletions contracts/agents-api/harnesses.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ syntactically or everything either upstream harness can theoretically perform.
| Non-default verbosity | Native/model-dependent support | No equivalent qualified; medium only |
| Public detailed Usage | Supported native counters | Native raw usage retained; public breakdown gap |
| V1 `self_hosted` daemon enrollment at `/workspace` | [Qualified deployment scope](user-managed-runtime-v1.md) | [Qualified deployment scope](user-managed-runtime-v1.md) |
| Deferred function discovery | Unqualified; explicit rejection | [Single-agent text/function profile](tool-search.md) |
| Structured output | Unqualified; explicit rejection | [Qualified single-agent function profile](structured-output.md) |
| Explicit reasoning, message images | Shared service gaps | Shared service gaps |
| Six Subagent reads | [Qualified scope](subagents.md) | [Qualified scope](subagents.md) |
Expand Down
6 changes: 5 additions & 1 deletion contracts/agents-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2720,7 +2720,11 @@ paths:
Session creation freezes concrete metadata and encrypted content atomically.
Skill-list omission inherits and a supplied list replaces; null overrides
and null version selectors remain unqualified and reject. Source deletion/default
updates cannot change committed Session Skill contents.
updates cannot change committed Session Skill contents. Deferred function
discovery uses type-only tool_search and per-function defer_loading in the
qualified single-agent Claude environment:none function profile, including
qualified inline image messages and text results. Other combinations remain
unqualified; see the operation coverage.
parameters:
- description: agents=v1
in: header
Expand Down
88 changes: 88 additions & 0 deletions contracts/agents-api/tool-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Deferred function discovery

Baseline: Python SDK 3.13.0, upstream
`d7c41efee1b0802b79f3f88a678ef2052b06e9ce`, Beta `agents=v1`.
This is bounded execution coverage, not complete Agents API compatibility.

## Contract and boundary

Saved and inline tools preserve the full function definition and `defer_loading`
(default false). The pinned Agents `tool_search` configuration has only `type`;
Responses-specific execution/parameter fields are not accepted here. The saved-Agent `PersistedAgentTool` union retains `tool_search`, while the
Session `AgentTool` response union omits it. Session/SSE resource projection follows
that pinned distinction; the full frozen configuration still includes it. Exact
hosted response behavior is unverified. The shared
Runtime carries search intent and each deferral flag. Native search and lazy schema
loading belong to the harness adapter. Core retains the frozen definitions and
existing public function calls, results and application receipts. The pinned Items
union contains no tool-search Item; do not invent one.

The first implementation is Claude SDK 0.3.269 / native 2.1.269, single Agent,
`environment:none`, medium verbosity, object-root function schemas and text results.
It supports a mixture of eager and deferred application functions with text or
previously qualified inline PNG/JPEG message inputs. The native MCP
server marks eager definitions `anthropic/alwaysLoad:true`; deferred definitions use
false and the adapter explicitly enables native ToolSearch. Only declared callbacks
and ToolSearch are allowed. No search index, callback protocol, provider proxy or
model loop is added to production.

Search-only, missing-search, duplicate-search, workspace, HTTP MCP, structured-output
and Subagent combinations remain unqualified. They are implementation/verification
gaps, not claimed upstream restrictions. Codex and MiniMax discovery remain gaps.
Unknown public combinations reject before execution; an actual Runtime must also
advertise the operation. An advertisement alone cannot qualify a public profile.

## Evidence and limitations

Native feasibility passed with Kimi `kimi-k3`: initial provider requests excluded
the deferred target schema; after a real native ToolSearch call that schema became
available, while an unrelated deferred schema stayed unloaded. The model used a
random required argument available only in that schema and returned the exact fresh
callback result. A new native process resumed the same native Session and called
it again with a new callback result. Existing discovered definitions remained in
that native history. Evidence: `~/.parsar/remediation/20260922/deferred-tools/claude-native-1790052750`.

A first test proxy omitted response Content-Encoding and failed after discovery;
the failed evidence is retained. The corrected test forwards unchanged real provider
responses. The proxy is test observation only and is not part of Runtime.

Codex source exposes deferred dynamic functions, but the available Kimi Responses
probe rejected native `tool_search`; the MiniMax sample did not produce discovery.
Neither establishes Codex qualification. These observations do not prove that all
models from either provider lack the feature.

The native harness owns dynamic model/provider policy. Known conflicting modes and
beta/search settings reject in the adapter. Its SDK has no reliable pre-input signal
proving actual deferral after opaque policy changes; init tool inventory is insufficient.
That detection gap and other providers/models remain unverified. No endpoint/model
allowlist or copied native policy evaluator is added to imply a stronger guarantee.

Public-chain real acceptance passed on 2026-09-22 with Kimi `kimi-k3`:
`tool-search-public-2315339108`, 97.83 seconds. The initial strict-SDK attempt
failed on the Session response union before any model request; that failure is
retained as `public-first.log`. Resource projection follows the fixed schema,
without relaxing SDK validation. Run `TestNativeToolSearchPublicExecution`
with the fixed official SDK, a real private model configuration, a real daemon and
PostgreSQL. It checks saved/inline configuration, mixed functions, result retries,
SSE/Items, cancellation, cold daemon continuation and tenant isolation. Independent review is required before release qualification.

Shared Go race checks and 132 Claude SDK checks passed. The required browser gate
passed 73 cases locally with Node22 and real Chrome. The server has no browser;
the remaining `make check` targets passed on zju against a dedicated PostgreSQL.
No database query/schema changed, so explicit sqlc generation is not applicable;
the full gate still verifies generated queries.

The image/discovery combination also passed through the same public chain in
`tool-search-public-2039155387` (`public-image-isolated-tests.log`). A random PNG
was submitted with the opening prompt and a different random PNG was submitted
while the deferred function waited. The answer identified the latest image's band
order and the fresh callback result; cancellation and cold continuation passed in
the same run. The existing PNG generator is shared with the image-input regression.
No new image admission restrictions or native lifecycle were needed.

The full server gate is `make -o check-web check`, with `make check-web` completed
locally on the same changes. Packaging initially caught an outdated expected Runtime
feature list; it was updated and the full server gate rerun successfully. Image
acceptance uses a separate dedicated database from the full gate. Failed setup
attempts (execution-owner lock and test-database naming guard) are retained; neither
reached model execution.
Loading
Loading