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
84 changes: 84 additions & 0 deletions docs/core-concepts/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -965,6 +965,90 @@ $registry->decoratorCount(); // number of registered decorators
$registry->clearDecorators(); // remove all decorators
```

## Queue Processing

AgentContext supports serialization for queue-based async processing. This enables dispatching agent jobs to Laravel queues while Atlas handles only the context serialization—consumers manage all persistence.

### Dispatching to Queue

```php
// Build context and serialize for queue transport
$context = new AgentContext(
variables: [
'user_name' => $user->name
],
metadata: [
'task_id' => $task->id,
'user_id' => $user->id
],
);

// You create a job that accepts AgentContext as a constructor argument
ProcessAgentJob::dispatch(
agentKey: 'my-agent',
input: 'Generate a report',
context: $context->toArray(),
);
```

### Processing in Job

Create a processing job

```php
use Atlasphp\Atlas\Agents\Support\AgentContext;

class ProcessAgentJob implements ShouldQueue
{
public function __construct(
public string $agentKey,
public string $input,
public array $context,
) {}

public function handle(): void
{
$context = AgentContext::fromArray($this->context);

$response = Atlas::agent($this->agentKey)
->withContext($context)
->chat($this->input);

// Handle response...
}
}
```

### Serialization Notes

The following properties are fully serialized:
- `messages` — Conversation history in array format
- `variables` — System prompt variable bindings
- `metadata` — Pipeline metadata
- `providerOverride` / `modelOverride` — Provider and model overrides
- `prismCalls` — Captured Prism method calls
- `tools` — Atlas tool class names

Runtime-only properties (not serialized):
- `prismMedia` — Media attachments (must be re-attached via `withMedia()`)
- `prismMessages` — Prism message objects (rebuilt at runtime)
- `mcpTools` — MCP tools (must be resolved at runtime)

For media attachments, store the file path in metadata and re-attach in your job:

```php
public function handle(): void
{
$context = AgentContext::fromArray($this->context);
$imagePath = $context->getMeta('image_path');

$response = Atlas::agent($this->agentKey)
->withContext($context)
->withMedia(Image::fromPath($imagePath))
->chat($this->input);
}
```

## API Reference

```php
Expand Down
47 changes: 47 additions & 0 deletions src/Agents/Support/AgentContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -325,4 +325,51 @@ public function clearMetadata(): self
$this->mcpTools,
);
}

/**
* Serialize context for queue transport.
*
* Note: Runtime-only properties (prismMedia, prismMessages, mcpTools) are
* not serialized as they contain Prism objects that cannot be serialized.
* These must be re-attached after deserialization if needed.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'messages' => $this->messages,
'variables' => $this->variables,
'metadata' => $this->metadata,
'provider_override' => $this->providerOverride,
'model_override' => $this->modelOverride,
'prism_calls' => $this->prismCalls,
'tools' => $this->tools,
];
}

/**
* Restore context from serialized data.
*
* Note: Runtime-only properties (prismMedia, prismMessages, mcpTools) are
* set to empty arrays. Use withMedia() or other builder methods to re-attach
* these after deserialization if needed.
*
* @param array<string, mixed> $data
*/
public static function fromArray(array $data): self
{
return new self(
messages: $data['messages'] ?? [],
variables: $data['variables'] ?? [],
metadata: $data['metadata'] ?? [],
providerOverride: $data['provider_override'] ?? null,
modelOverride: $data['model_override'] ?? null,
prismCalls: $data['prism_calls'] ?? [],
prismMedia: [],
prismMessages: [],
tools: $data['tools'] ?? [],
mcpTools: [],
);
}
}
38 changes: 38 additions & 0 deletions src/Agents/Support/PendingAgentRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,44 @@ public function mergeMcpTools(array $tools): static
return $clone;
}

/**
* Apply a deserialized AgentContext to this request.
*
* Useful for queue processing where context was serialized with toArray()
* and restored with fromArray(). Applies all serializable properties:
* messages, variables, metadata, provider/model overrides, prism calls, and tools.
*
* Runtime-only properties (prismMedia, prismMessages, mcpTools) are not applied
* as they cannot be serialized. Use withMedia() or withMcpTools() to re-attach
* these after applying the context.
*
* ```php
* $context = AgentContext::fromArray($serializedContext);
* $response = Atlas::agent('my-agent')
* ->withContext($context)
* ->withMedia($image) // Re-attach media if needed
* ->chat($input);
* ```
*/
public function withContext(AgentContext $context): static
{
$clone = clone $this;

// Apply serializable properties
$clone->messages = $context->messages;
$clone->prismMessages = $context->prismMessages;
$clone->variables = $context->variables;
$clone->metadata = $context->metadata;
$clone->providerOverride = $context->providerOverride;
$clone->modelOverride = $context->modelOverride;
$clone->prismCalls = $context->prismCalls;
$clone->tools = $context->tools;
$clone->mcpTools = $context->mcpTools;
$clone->prismMedia = $context->prismMedia;

return $clone;
}

/**
* Execute a blocking chat with the configured agent.
*
Expand Down
146 changes: 146 additions & 0 deletions tests/Unit/Agents/AgentContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,149 @@
expect($newContext->providerOverride)->toBe('anthropic');
expect($newContext->modelOverride)->toBe('claude-3');
});

// === Serialization Tests ===

test('toArray serializes all serializable properties', function () {
$context = new AgentContext(
messages: [['role' => 'user', 'content' => 'Hello']],
variables: ['user_id' => 123],
metadata: ['task_id' => 'abc'],
providerOverride: 'anthropic',
modelOverride: 'claude-3-opus',
prismCalls: [['method' => 'withMaxSteps', 'args' => [10]]],
tools: ['App\\Tools\\MyTool'],
);

$array = $context->toArray();

expect($array)->toBe([
'messages' => [['role' => 'user', 'content' => 'Hello']],
'variables' => ['user_id' => 123],
'metadata' => ['task_id' => 'abc'],
'provider_override' => 'anthropic',
'model_override' => 'claude-3-opus',
'prism_calls' => [['method' => 'withMaxSteps', 'args' => [10]]],
'tools' => ['App\\Tools\\MyTool'],
]);
});

test('fromArray restores context from array', function () {
$data = [
'messages' => [['role' => 'assistant', 'content' => 'Hi there']],
'variables' => ['name' => 'John'],
'metadata' => ['session' => 'xyz'],
'provider_override' => 'openai',
'model_override' => 'gpt-4o',
'prism_calls' => [['method' => 'usingTemperature', 'args' => [0.7]]],
'tools' => ['App\\Tools\\ToolA', 'App\\Tools\\ToolB'],
];

$context = AgentContext::fromArray($data);

expect($context->messages)->toBe([['role' => 'assistant', 'content' => 'Hi there']]);
expect($context->variables)->toBe(['name' => 'John']);
expect($context->metadata)->toBe(['session' => 'xyz']);
expect($context->providerOverride)->toBe('openai');
expect($context->modelOverride)->toBe('gpt-4o');
expect($context->prismCalls)->toBe([['method' => 'usingTemperature', 'args' => [0.7]]]);
expect($context->tools)->toBe(['App\\Tools\\ToolA', 'App\\Tools\\ToolB']);
});

test('toArray and fromArray round-trip preserves data', function () {
$original = new AgentContext(
messages: [
['role' => 'user', 'content' => 'First message'],
['role' => 'assistant', 'content' => 'Response'],
],
variables: ['company' => 'Acme', 'tier' => 'premium'],
metadata: ['request_id' => 'req-123', 'user_id' => 456],
providerOverride: 'anthropic',
modelOverride: 'claude-sonnet-4-20250514',
prismCalls: [
['method' => 'withMaxSteps', 'args' => [5]],
['method' => 'usingTemperature', 'args' => [0.5]],
],
tools: ['App\\Tools\\SearchTool', 'App\\Tools\\CalculateTool'],
);

$restored = AgentContext::fromArray($original->toArray());

expect($restored->messages)->toBe($original->messages);
expect($restored->variables)->toBe($original->variables);
expect($restored->metadata)->toBe($original->metadata);
expect($restored->providerOverride)->toBe($original->providerOverride);
expect($restored->modelOverride)->toBe($original->modelOverride);
expect($restored->prismCalls)->toBe($original->prismCalls);
expect($restored->tools)->toBe($original->tools);
});

test('fromArray handles missing keys with defaults', function () {
$context = AgentContext::fromArray([]);

expect($context->messages)->toBe([]);
expect($context->variables)->toBe([]);
expect($context->metadata)->toBe([]);
expect($context->providerOverride)->toBeNull();
expect($context->modelOverride)->toBeNull();
expect($context->prismCalls)->toBe([]);
expect($context->tools)->toBe([]);
});

test('fromArray sets non-serializable properties to empty arrays', function () {
$data = [
'messages' => [['role' => 'user', 'content' => 'Test']],
'variables' => ['key' => 'value'],
];

$context = AgentContext::fromArray($data);

expect($context->prismMedia)->toBe([]);
expect($context->prismMessages)->toBe([]);
expect($context->mcpTools)->toBe([]);
});

test('toArray excludes runtime-only properties', function () {
$mockImage = Mockery::mock(\Prism\Prism\ValueObjects\Media\Image::class);
$mockMessage = Mockery::mock(\Prism\Prism\ValueObjects\Messages\UserMessage::class);
$mockTool = Mockery::mock(\Prism\Prism\Tool::class);

$context = new AgentContext(
messages: [['role' => 'user', 'content' => 'Hello']],
variables: ['var' => 'value'],
prismMedia: [$mockImage],
prismMessages: [$mockMessage],
mcpTools: [$mockTool],
);

$array = $context->toArray();

expect($array)->not->toHaveKey('prism_media');
expect($array)->not->toHaveKey('prism_messages');
expect($array)->not->toHaveKey('mcp_tools');
expect(array_keys($array))->toBe([
'messages',
'variables',
'metadata',
'provider_override',
'model_override',
'prism_calls',
'tools',
]);
});

test('fromArray partial data preserves specified values', function () {
$data = [
'messages' => [['role' => 'user', 'content' => 'Hello']],
'provider_override' => 'anthropic',
// Other keys intentionally missing
];

$context = AgentContext::fromArray($data);

expect($context->messages)->toBe([['role' => 'user', 'content' => 'Hello']]);
expect($context->providerOverride)->toBe('anthropic');
expect($context->variables)->toBe([]);
expect($context->metadata)->toBe([]);
expect($context->modelOverride)->toBeNull();
});
Loading