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
43 changes: 35 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ databases, credentials and migrations. The product uses Core exclusively; it has
merely for new names or directories. Replacements may retire obsolete private
interfaces and history backfills in bounded PRs; this does not authorize deleting
product data or changing unrelated product behavior.
- Every concrete harness interaction goes through the common Runtime contract
and its adapter. Extend that contract minimally when a current operation cannot
be expressed; never put native capability logic or transport conversion into
Core handlers, storage or scheduling. Qualify public workflows through the same
shared chain; direct native probes establish feasibility only.
- Keep engine-specific types, process management and protocol translation inside
execution adapters. The public API and persistence/application core must not
interpret Parsar product payloads or depend on one engine's native item types.
Expand All @@ -114,10 +119,14 @@ databases, credentials and migrations. The product uses Core exclusively; it has
- Maintain tasks, priorities and evidence in the Feishu board. Register issues
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 it done, reread the full
board and choose the next task by value, dependencies, risk and effort. Agent API
protocol and atomic execution work takes priority over product integration, UI
work and business Team orchestration. Prioritize a sound architecture skeleton
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
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
delay the main work. Required checks and material correctness guarantees apply.
Expand Down Expand Up @@ -158,11 +167,14 @@ The current MVP covers Codex, Claude Code and MiniMax Code through the shared
single-Agent path: Session
creation, environment preparation, native execution, files/artifacts, cancellation,
reconnection/recovery queries, and standalone deployment acceptance. Select each
bounded task from the complete board; nonblocking local improvements stay queued.
bounded child from the complete board within the selected concise-TODO task;
nonblocking local improvements stay queued.
Authentication, tenant/credential isolation, state consistency and data loss remain
material acceptance requirements. Optional feature equality is not required. After
the three profiles pass merged-main validation, publish the results, limitations
and backlog, then stop development until new user direction. Additional harness implementations remain queued. The separately authorized
and backlog. The user has since renewed continuous Core delivery: select and
finish large tasks from the concise TODO, retaining bounded child acceptance.
Additional harness implementations remain queued. The separately authorized
Subagent batch targets the six read operations across these three harnesses and
does not change the complete pinned protocol target.

Expand Down Expand Up @@ -204,9 +216,24 @@ forwarding. The explicit daemon-executor decision supersedes the previous native
executor interoperability requirement. The superseded execution route is removed;
retain reusable filesystem helpers,
necessary regressions and historical evidence without a compatibility layer.
The private daemon wire protocol is 0.3.0 after removal of remote execution fields.
The private daemon wire protocol is 0.4.0. Initial, prepared and active input use
the same ordered MessageInput contract, replacing scalar prompts and attachments.
User-message boundaries and text/image order remain intact through Core and the
Runtime wire; adapters own native conversion and receipt aggregation. Text-only
transports reject image content rather than dropping it. Codex has a flat native
input list and uses blank-line separators between messages; this does not preserve
independent native user-message boundaries. No old wire fallback is maintained.
Deploy Core and daemon together; the existing major/minor WebSocket check rejects
0.2 peers before dispatch rather than ignoring their removed configuration.
older major/minor peers before dispatch rather than ignoring removed fields.
The independently packaged Claude bridge uses protocol 2 for ordered input;
readiness rejects packages reporting the old string-input protocol.
Image-bearing messages require a qualified profile/placement before persistence
and image support from the selected Runtime before native delivery. These checks
apply to that operation only; ordinary text retains offline queueing. Initial,
prepared and active paths use the same content and preserve receipt ownership.
The qualified public profile is inline PNG/JPEG on Codex/Claude `none`; workspace
images, MiniMax images and remote URLs remain explicit implementation gaps. Core
does not fetch or transform media. See [message input coverage](contracts/agents-api/message-input.md).

User-managed onboarding creates a `self_hosted` Session first, then passes its
Environment ID and unchanged `remote_url` to our Runtime with connect-only
Expand Down
4 changes: 0 additions & 4 deletions apps/parsar-daemon/internal/agent/claudecode/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ type (
Envelope = proto.Envelope
)

func BuildUserMessageForTest(prompt string, attachments []proto.PromptAttachment) ([]byte, error) {
return buildUserMessageWithAttachments(prompt, attachments)
}

// BuildAskUserToolResultForTest exposes the daemon-side tool_result
// builder so ask_test.go can lock in the JSON shape claude's stdin
// expects.
Expand Down
41 changes: 41 additions & 0 deletions apps/parsar-daemon/internal/agent/claudecode/message_input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package claudecode

import (
"bytes"
"encoding/json"
"fmt"
"github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto"
"strings"
)

func buildOrderedUserMessages(input proto.MessageInput) ([]byte, error) {
if err := input.Validate(); err != nil {
return nil, err
}
var output bytes.Buffer
encoder := json.NewEncoder(&output)
encoder.SetEscapeHTML(false)
for _, message := range input {
var blocks []userContentBlock
for _, part := range message.Content {
if part.Type == "input_text" {
blocks = append(blocks, userContentBlock{Type: "text", Text: *part.Text})
continue
}
header, data, ok := strings.Cut(*part.ImageURL, ",")
if !ok || (header != "data:image/png;base64" && header != "data:image/jpeg;base64") {
return nil, fmt.Errorf("claudecode: unsupported image reference")
}
mime := strings.TrimSuffix(strings.TrimPrefix(header, "data:"), ";base64")
blocks = append(blocks, userContentBlock{Type: "image", Source: &userContentSource{Type: "base64", MediaType: mime, Data: data}})
}
var content any = blocks
if len(blocks) == 1 && blocks[0].Type == "text" {
content = blocks[0].Text
}
if err := encoder.Encode(userMessage{Type: "user", Message: userMessageContent{Role: "user", Content: content}}); err != nil {
return nil, err
}
}
return output.Bytes(), nil
}
40 changes: 40 additions & 0 deletions apps/parsar-daemon/internal/agent/claudecode/message_input_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package claudecode

import (
"bytes"
"encoding/json"
"github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto"
"testing"
)

func TestOrderedMessageInputNativeConversion(t *testing.T) {
image := "data:image/png;base64,aW1hZ2U="
input := proto.TextInput(" before ")
input[0].Content = append(input[0].Content, proto.InputContent{Type: "input_image", ImageURL: &image}, proto.TextInput(" after ")[0].Content[0])
input = append(input, proto.TextInput("next")...)
raw, err := buildOrderedUserMessages(input)
if err != nil {
t.Fatal(err)
}
lines := bytes.Split(bytes.TrimSpace(raw), []byte("\n"))
if len(lines) != 2 {
t.Fatalf("message boundary lost: %s", raw)
}
var first struct {
Message struct{ Content []userContentBlock }
}
if err := json.Unmarshal(lines[0], &first); err != nil {
t.Fatal(err)
}
content := first.Message.Content
if len(content) != 3 || content[0].Text != " before " || content[1].Source == nil || content[1].Source.Data != "aW1hZ2U=" || content[2].Text != " after " {
t.Fatalf("native order changed: %s", raw)
}
image = "https://unsupported.example/image.png"
if _, err := buildOrderedUserMessages(input); err == nil {
t.Fatal("unsupported image reference accepted")
}
if _, err := buildOrderedUserMessages(proto.TextInput("")); err == nil {
t.Fatal("empty message accepted")
}
}
62 changes: 0 additions & 62 deletions apps/parsar-daemon/internal/agent/claudecode/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,12 @@
package claudecode

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"

"github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto"
)

// BuildResult is the output of BuildArgs. Cleanup is always non-nil
Expand Down Expand Up @@ -286,61 +282,3 @@ type userContentSource struct {
MediaType string `json:"media_type"`
Data string `json:"data"`
}

func buildUserMessage(prompt string) ([]byte, error) {
return buildUserMessageWithAttachments(prompt, nil)
}

// buildUserMessageWithAttachments is the multimodal-aware variant. With
// no attachments, the output is byte-identical to the bare-string
// Content path so existing log greps for prompt content keep working.
// Non-image attachments are dropped — Claude Code SDK only understands
// the image block shape on stdin.
func buildUserMessageWithAttachments(prompt string, attachments []proto.PromptAttachment) ([]byte, error) {
if prompt == "" && len(attachments) == 0 {
return nil, errors.New("claudecode: empty prompt")
}
var content any
if len(attachments) == 0 {
content = prompt
} else {
blocks := make([]userContentBlock, 0, len(attachments)+1)
if prompt != "" {
blocks = append(blocks, userContentBlock{Type: "text", Text: prompt})
}
for _, att := range attachments {
if att.Kind != "image" || att.DataBase64 == "" {
continue
}
mime := att.MIME
if mime == "" {
mime = "image/png"
}
blocks = append(blocks, userContentBlock{
Type: "image",
Source: &userContentSource{
Type: "base64",
MediaType: mime,
Data: att.DataBase64,
},
})
}
if len(blocks) == 0 {
return nil, errors.New("claudecode: empty prompt after dropping unsupported attachments")
}
content = blocks
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
if err := enc.Encode(userMessage{
Type: "user",
Message: userMessageContent{
Role: "user",
Content: content,
},
}); err != nil {
return nil, fmt.Errorf("claudecode: marshal user message: %w", err)
}
return buf.Bytes(), nil
}
120 changes: 0 additions & 120 deletions apps/parsar-daemon/internal/agent/claudecode/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"testing"

"github.com/MiniMax-AI-Dev/parsar/apps/parsar-daemon/internal/agent/claudecode"
"github.com/MiniMax-AI-Dev/parsar/internal/agentdaemon/proto"
)

func TestBuildArgsBaseHasStreamFlags(t *testing.T) {
Expand Down Expand Up @@ -284,122 +283,3 @@ func containsPair(args []string, flag, value string) bool {
}
return false
}

func TestBuildUserMessage_TextOnlyKeepsBareStringContent(t *testing.T) {
// Backwards compat: no attachments → Content stays a bare string.
raw, err := claudecode.BuildUserMessageForTest("hello world", nil)
if err != nil {
t.Fatalf("BuildUserMessageForTest: %v", err)
}
var msg struct {
Type string `json:"type"`
Message struct {
Role string `json:"role"`
Content json.RawMessage `json:"content"`
} `json:"message"`
}
if err := json.Unmarshal(raw, &msg); err != nil {
t.Fatalf("unmarshal: %v\nbody=%s", err, raw)
}
if msg.Type != "user" || msg.Message.Role != "user" {
t.Fatalf("unexpected envelope: %+v", msg)
}
if string(msg.Message.Content) != `"hello world"` {
t.Fatalf("Content not bare string: %s", msg.Message.Content)
}
}

func TestBuildUserMessage_WithImageEmitsContentBlocks(t *testing.T) {
att := []proto.PromptAttachment{
{Kind: "image", MIME: "image/png", DataBase64: "AAAA"},
{Kind: "image", MIME: "image/jpeg", DataBase64: "BBBB"},
}
raw, err := claudecode.BuildUserMessageForTest("look at this", att)
if err != nil {
t.Fatalf("BuildUserMessageForTest: %v", err)
}
var msg struct {
Message struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
Source *struct {
Type string `json:"type"`
MediaType string `json:"media_type"`
Data string `json:"data"`
} `json:"source"`
} `json:"content"`
} `json:"message"`
}
if err := json.Unmarshal(raw, &msg); err != nil {
t.Fatalf("unmarshal: %v\nbody=%s", err, raw)
}
if len(msg.Message.Content) != 3 {
t.Fatalf("expected 3 blocks (text+2 images), got %d: %s", len(msg.Message.Content), raw)
}
if msg.Message.Content[0].Type != "text" || msg.Message.Content[0].Text != "look at this" {
t.Errorf("block 0 = %+v", msg.Message.Content[0])
}
for i, want := range []string{"image/png", "image/jpeg"} {
b := msg.Message.Content[i+1]
if b.Type != "image" || b.Source == nil {
t.Errorf("block %d not image: %+v", i+1, b)
continue
}
if b.Source.Type != "base64" || b.Source.MediaType != want {
t.Errorf("block %d source = %+v", i+1, b.Source)
}
}
}

func TestBuildUserMessage_EmptyPromptWithImageStillValid(t *testing.T) {
// Pure-image-no-caption is a valid message — user pastes a
// screenshot without typing anything.
att := []proto.PromptAttachment{
{Kind: "image", MIME: "image/png", DataBase64: "AAAA"},
}
raw, err := claudecode.BuildUserMessageForTest("", att)
if err != nil {
t.Fatalf("BuildUserMessageForTest: %v", err)
}
var msg struct {
Message struct {
Content []struct {
Type string `json:"type"`
} `json:"content"`
} `json:"message"`
}
if err := json.Unmarshal(raw, &msg); err != nil {
t.Fatalf("unmarshal: %v\nbody=%s", err, raw)
}
if len(msg.Message.Content) != 1 || msg.Message.Content[0].Type != "image" {
t.Fatalf("expected single image block, got %+v", msg.Message.Content)
}
}

func TestBuildUserMessage_UnsupportedAttachmentsDropped(t *testing.T) {
// Non-image kinds aren't representable on stdin; dropped silently.
att := []proto.PromptAttachment{
{Kind: "file", MIME: "text/plain", DataBase64: "AAAA"},
{Kind: "image", DataBase64: ""},
}
raw, err := claudecode.BuildUserMessageForTest("hi", att)
if err != nil {
t.Fatalf("BuildUserMessageForTest: %v", err)
}
if !strings.Contains(string(raw), `"hi"`) {
t.Errorf("prompt text missing: %s", raw)
}
}

func TestBuildUserMessage_EmptyPromptAndNoImagesErrors(t *testing.T) {
if _, err := claudecode.BuildUserMessageForTest("", nil); err == nil {
t.Fatal("expected error for empty prompt + no attachments")
}
att := []proto.PromptAttachment{
{Kind: "file", DataBase64: "X"},
}
if _, err := claudecode.BuildUserMessageForTest("", att); err == nil {
t.Fatal("expected error when all attachments dropped + empty prompt")
}
}
Loading
Loading